@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,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Word wrapping: the offsets one logical line is cut at so its pieces fit a
|
|
3
|
+
* cell budget.
|
|
4
|
+
*
|
|
5
|
+
* This is deliberately not in `cells.ts` next door. That module answers
|
|
6
|
+
* geometry — how wide is this, where does cell column N fall — and knows
|
|
7
|
+
* nothing about language. This one answers where a line may *break*, which is
|
|
8
|
+
* a fact about words, and the only thing it borrows from geometry is how to
|
|
9
|
+
* measure one.
|
|
10
|
+
*
|
|
11
|
+
* It returns offsets rather than pieces because the caller holds styled
|
|
12
|
+
* `Segment[]`, not a string: cutting the text here would drop every span
|
|
13
|
+
* attached to it. Offsets are the one representation both sides can act on —
|
|
14
|
+
* `Segment.divide` cuts the styled line at exactly the places this function
|
|
15
|
+
* found in the plain one.
|
|
16
|
+
*
|
|
17
|
+
* Wrapping is the *first* step, and overflow is the last resort applied to a
|
|
18
|
+
* line still too wide once wrapping is done. That order is the whole reason a
|
|
19
|
+
* long sentence in a table cell grows the row while an unbreakable word in the
|
|
20
|
+
* same cell still ellipsizes.
|
|
21
|
+
*/
|
|
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[];
|
|
28
|
+
/**
|
|
29
|
+
* The cell offsets `text` should be cut at to wrap it into lines of at most
|
|
30
|
+
* `width` cells, for `Segment.divide` to apply to the styled line.
|
|
31
|
+
*
|
|
32
|
+
* `fold` decides what happens to a word that fits on no line at all: folded,
|
|
33
|
+
* it is chopped across as many lines as it needs; unfolded, it is left whole
|
|
34
|
+
* on its own line for the caller's overflow method to cut. Only the `fold`
|
|
35
|
+
* overflow method folds — `crop` and `ellipsis` want the word intact so they
|
|
36
|
+
* can truncate it, which is what makes an unbreakable word ellipsize while the
|
|
37
|
+
* sentence around it merely wraps.
|
|
38
|
+
*
|
|
39
|
+
* Requires `width >= 1`. A zero-cell canvas gets no cuts, because "no cut
|
|
40
|
+
* fits" and "no cell fits" are different facts and only one of them is a list
|
|
41
|
+
* of offsets — the caller answers the second one by cropping the single
|
|
42
|
+
* uncut piece to nothing, which is what every overflow method does there.
|
|
43
|
+
*/
|
|
44
|
+
export declare function divideLine(text: string, width: CellCol, options: {
|
|
45
|
+
fold: boolean;
|
|
46
|
+
}): CellCol[];
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Word wrapping: the offsets one logical line is cut at so its pieces fit a
|
|
3
|
+
* cell budget.
|
|
4
|
+
*
|
|
5
|
+
* This is deliberately not in `cells.ts` next door. That module answers
|
|
6
|
+
* geometry — how wide is this, where does cell column N fall — and knows
|
|
7
|
+
* nothing about language. This one answers where a line may *break*, which is
|
|
8
|
+
* a fact about words, and the only thing it borrows from geometry is how to
|
|
9
|
+
* measure one.
|
|
10
|
+
*
|
|
11
|
+
* It returns offsets rather than pieces because the caller holds styled
|
|
12
|
+
* `Segment[]`, not a string: cutting the text here would drop every span
|
|
13
|
+
* attached to it. Offsets are the one representation both sides can act on —
|
|
14
|
+
* `Segment.divide` cuts the styled line at exactly the places this function
|
|
15
|
+
* found in the plain one.
|
|
16
|
+
*
|
|
17
|
+
* Wrapping is the *first* step, and overflow is the last resort applied to a
|
|
18
|
+
* line still too wide once wrapping is done. That order is the whole reason a
|
|
19
|
+
* long sentence in a table cell grows the row while an unbreakable word in the
|
|
20
|
+
* same cell still ellipsizes.
|
|
21
|
+
*/
|
|
22
|
+
import { cellLen, chopCells, asCellCol } from "./cells.js";
|
|
23
|
+
/**
|
|
24
|
+
* Each word of `text` in order, where a "word" carries its own trailing
|
|
25
|
+
* whitespace — the run of spaces that follows it belongs to it, because that
|
|
26
|
+
* is the whitespace a break at the *next* word leaves hanging past the edge.
|
|
27
|
+
*
|
|
28
|
+
* The matches tile the text: each begins exactly where the last ended, so a
|
|
29
|
+
* caller can track absolute offsets by accumulating widths alone. Text with no
|
|
30
|
+
* non-whitespace character yields nothing, which is the honest answer — there
|
|
31
|
+
* is nowhere to break.
|
|
32
|
+
*/
|
|
33
|
+
function* words(text) {
|
|
34
|
+
// Constructed per call rather than hoisted: a sticky regex carries its
|
|
35
|
+
// `lastIndex` as mutable state, and a shared one would corrupt any two
|
|
36
|
+
// interleaved walks. [LAW:no-shared-mutable-globals]
|
|
37
|
+
const re = /\s*\S+\s*/y;
|
|
38
|
+
for (let match = re.exec(text); match !== null; match = re.exec(text)) {
|
|
39
|
+
yield match[0];
|
|
40
|
+
}
|
|
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
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* A word chopped into pieces of at most `width` cells, as the reference chops
|
|
59
|
+
* it.
|
|
60
|
+
*
|
|
61
|
+
* `chopCells` next door and Rich's `chop_cells` disagree in exactly one place.
|
|
62
|
+
* Rich opens a new line for any glyph that does not fit and emits whatever line
|
|
63
|
+
* it was already on, so a word beginning with a glyph wider than the entire
|
|
64
|
+
* canvas folds with an empty piece in front of it — and that piece reaches the
|
|
65
|
+
* output as a blank line. `chopCells` force-takes the glyph instead and never
|
|
66
|
+
* returns an empty piece.
|
|
67
|
+
*
|
|
68
|
+
* Restoring the blank is what keeps `test/core/text-wrap.golden.txt`
|
|
69
|
+
* regenerable from the reference. A fixture is only an oracle while the command
|
|
70
|
+
* in its header reproduces it byte for byte; smoothing away one byte the
|
|
71
|
+
* reference emits costs that, everywhere, to tidy a blank line reachable only
|
|
72
|
+
* by folding a two-cell glyph onto a one-cell canvas.
|
|
73
|
+
*/
|
|
74
|
+
function foldWord(word, width) {
|
|
75
|
+
const pieces = chopCells(word, width);
|
|
76
|
+
// `chopCells` overshoots the budget only when it force-takes one glyph, so an
|
|
77
|
+
// over-wide first piece is exactly the case Rich opens a blank line for.
|
|
78
|
+
return cellLen(pieces[0]) > width ? ["", ...pieces] : pieces;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* The cell offsets `text` should be cut at to wrap it into lines of at most
|
|
82
|
+
* `width` cells, for `Segment.divide` to apply to the styled line.
|
|
83
|
+
*
|
|
84
|
+
* `fold` decides what happens to a word that fits on no line at all: folded,
|
|
85
|
+
* it is chopped across as many lines as it needs; unfolded, it is left whole
|
|
86
|
+
* on its own line for the caller's overflow method to cut. Only the `fold`
|
|
87
|
+
* overflow method folds — `crop` and `ellipsis` want the word intact so they
|
|
88
|
+
* can truncate it, which is what makes an unbreakable word ellipsize while the
|
|
89
|
+
* sentence around it merely wraps.
|
|
90
|
+
*
|
|
91
|
+
* Requires `width >= 1`. A zero-cell canvas gets no cuts, because "no cut
|
|
92
|
+
* fits" and "no cell fits" are different facts and only one of them is a list
|
|
93
|
+
* of offsets — the caller answers the second one by cropping the single
|
|
94
|
+
* uncut piece to nothing, which is what every overflow method does there.
|
|
95
|
+
*/
|
|
96
|
+
export function divideLine(text, width, options) {
|
|
97
|
+
if (width <= 0)
|
|
98
|
+
return [];
|
|
99
|
+
const cuts = [];
|
|
100
|
+
// A cut at the start of the text is not a cut: it would open the line with
|
|
101
|
+
// an empty piece. [LAW:single-enforcer] for that rule, so neither arm below
|
|
102
|
+
// has to restate it.
|
|
103
|
+
const cutAt = (cell) => {
|
|
104
|
+
if (cell > 0)
|
|
105
|
+
cuts.push(asCellCol(cell));
|
|
106
|
+
};
|
|
107
|
+
let used = 0; // cells already committed to the line being filled
|
|
108
|
+
let wordStart = 0; // absolute cell offset where the current word begins
|
|
109
|
+
for (const word of words(text)) {
|
|
110
|
+
const wordWidth = widthOf(word);
|
|
111
|
+
if (width - used >= wordWidth) {
|
|
112
|
+
used += cellLen(word);
|
|
113
|
+
}
|
|
114
|
+
else if (options.fold && wordWidth > width) {
|
|
115
|
+
// No line, however empty, can hold this word whole, and folding is
|
|
116
|
+
// allowed — so it is chopped across as many lines as it needs. Unfolded,
|
|
117
|
+
// it takes the arm below and is left whole for the overflow method.
|
|
118
|
+
const pieces = foldWord(word, width);
|
|
119
|
+
let pieceStart = wordStart;
|
|
120
|
+
for (let index = 0; index < pieces.length; index += 1) {
|
|
121
|
+
cutAt(pieceStart);
|
|
122
|
+
const piece = pieces[index];
|
|
123
|
+
// The last piece is what the *next* word finds already on the line;
|
|
124
|
+
// the rest are whole lines of their own.
|
|
125
|
+
if (index === pieces.length - 1)
|
|
126
|
+
used = cellLen(piece);
|
|
127
|
+
else
|
|
128
|
+
pieceStart += cellLen(piece);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
// It does not fit here. Either it fits on a line of its own, or nothing
|
|
133
|
+
// can hold it and the caller's overflow method will cut it there.
|
|
134
|
+
cutAt(wordStart);
|
|
135
|
+
used = cellLen(word);
|
|
136
|
+
}
|
|
137
|
+
wordStart += cellLen(word);
|
|
138
|
+
}
|
|
139
|
+
return cuts;
|
|
140
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App — the runtime a terminal application runs in: it takes the terminal,
|
|
3
|
+
* paints one renderable on it every frame, and hands the terminal back on
|
|
4
|
+
* every path out.
|
|
5
|
+
*
|
|
6
|
+
* [LAW:single-enforcer] Every change this runtime makes to the terminal —
|
|
7
|
+
* the alternate screen, pointer reporting, the hidden cursor, raw mode — is
|
|
8
|
+
* made by `enter` and
|
|
9
|
+
* undone by `leave`, and nothing else touches them. Stopping, suspending, an
|
|
10
|
+
* error in a frame and the program ending by a signal or a crash all hand the
|
|
11
|
+
* terminal back through `leave`, so there is one list of what to undo and no
|
|
12
|
+
* exit path can hold a shorter one.
|
|
13
|
+
*
|
|
14
|
+
* [LAW:one-source-of-truth] The frame on screen is one value: the rows last
|
|
15
|
+
* painted, carrying on every cell where its owner drew it (`core/anchor`).
|
|
16
|
+
* Nothing about it is cached beside it, so a resize invalidates nothing — it
|
|
17
|
+
* is input to the next frame, which replaces the rows and their anchors at
|
|
18
|
+
* once. An event arriving before that repaint lands on the frame on screen,
|
|
19
|
+
* which is the one the user pointed at.
|
|
20
|
+
*
|
|
21
|
+
* [LAW:locality-or-seam] The terminal's bytes are identical under every host
|
|
22
|
+
* and live here: entering and leaving the alternate screen, the cursor, the
|
|
23
|
+
* frame's encoding. What differs per host — whether the program can end
|
|
24
|
+
* under us, how it is suspended — is the `TerminalHost`'s, so the same app
|
|
25
|
+
* runs in node and in a browser with no path of its own for either.
|
|
26
|
+
*/
|
|
27
|
+
import { type Clock, type FrameRate } from "../core/clock.js";
|
|
28
|
+
import { type ConsoleOptions } from "../core/console.js";
|
|
29
|
+
import { Segment } from "../core/segment.js";
|
|
30
|
+
import { type Surface } from "../core/paint.js";
|
|
31
|
+
import { type Renderable } from "../core/protocol.js";
|
|
32
|
+
import type { Unsubscribe } from "../core/subscription.js";
|
|
33
|
+
import type { TerminalHost } from "./terminal-host.js";
|
|
34
|
+
/**
|
|
35
|
+
* Where an app is in its life. `idle` has not run; `running` holds the
|
|
36
|
+
* terminal and paints; `suspended` has handed it back until the host resumes
|
|
37
|
+
* the program; `stopped` has handed it back for good.
|
|
38
|
+
*/
|
|
39
|
+
export type AppPhase = "idle" | "running" | "suspended" | "stopped";
|
|
40
|
+
declare const DRAW_OPTIONS: readonly ["asciiOnly", "theme", "onStyleError", "markup", "highlight", "highlighter"];
|
|
41
|
+
type DrawOption = (typeof DRAW_OPTIONS)[number];
|
|
42
|
+
type DrawOptions = Readonly<Pick<ConsoleOptions, DrawOption>>;
|
|
43
|
+
export interface AppOptions extends DrawOptions {
|
|
44
|
+
/** The terminal the app runs on. The app starts and stops it. */
|
|
45
|
+
readonly host: TerminalHost;
|
|
46
|
+
readonly surface: Surface;
|
|
47
|
+
/**
|
|
48
|
+
* The app's root renderable, asked for once per frame, so a view of the
|
|
49
|
+
* app's state is drawn from the state as it is at that frame. `t` is the
|
|
50
|
+
* frame's time in seconds on the app's `clock`, which is what an effect is
|
|
51
|
+
* sampled at.
|
|
52
|
+
*/
|
|
53
|
+
readonly view: (t: number) => Renderable;
|
|
54
|
+
/** What the app reads the time from and ticks on. The platform's, unless given. */
|
|
55
|
+
readonly clock?: Clock;
|
|
56
|
+
/** How often the app paints while something is animating. 30 frames a second, unless given. */
|
|
57
|
+
readonly rate?: FrameRate;
|
|
58
|
+
}
|
|
59
|
+
export declare class App {
|
|
60
|
+
private readonly host;
|
|
61
|
+
private readonly surface;
|
|
62
|
+
private readonly view;
|
|
63
|
+
private readonly clock;
|
|
64
|
+
private readonly rate;
|
|
65
|
+
private readonly animations;
|
|
66
|
+
private stopTicking;
|
|
67
|
+
private readonly console;
|
|
68
|
+
private _phase;
|
|
69
|
+
private _frame;
|
|
70
|
+
private readonly painter;
|
|
71
|
+
private refreshQueued;
|
|
72
|
+
private subscriptions;
|
|
73
|
+
private readonly paintHandlers;
|
|
74
|
+
private settle;
|
|
75
|
+
constructor(options: AppOptions);
|
|
76
|
+
get phase(): AppPhase;
|
|
77
|
+
/** The rows on screen now, as painted: what a pointer event lands on. */
|
|
78
|
+
get frame(): readonly (readonly Segment[])[];
|
|
79
|
+
/**
|
|
80
|
+
* Take the terminal, paint the first frame, and resolve when the app stops
|
|
81
|
+
* — or reject with the error a frame threw or `fail` was given, the terminal
|
|
82
|
+
* handed back first.
|
|
83
|
+
* When the program ends under the app, `run` does not settle: nothing after
|
|
84
|
+
* it runs, as nothing after it would have without the app. An app runs once.
|
|
85
|
+
*/
|
|
86
|
+
run(): Promise<void>;
|
|
87
|
+
/**
|
|
88
|
+
* Paint a new frame from the view, once, in a later task: every change made
|
|
89
|
+
* before then lands in the one frame.
|
|
90
|
+
*
|
|
91
|
+
* [LAW:no-ambient-temporal-coupling] A task, not a microtask. Input is read
|
|
92
|
+
* between tasks, so a view whose every frame asks for another still hears
|
|
93
|
+
* its keys — Ctrl+C included — where a microtask chain would starve them
|
|
94
|
+
* for as long as it ran.
|
|
95
|
+
*/
|
|
96
|
+
refresh(): void;
|
|
97
|
+
/**
|
|
98
|
+
* Paint at the app's `rate` until the returned function is called: what an
|
|
99
|
+
* effect sampled at the frame's time asks for while it moves. The app ticks
|
|
100
|
+
* while anything is animating and it holds the terminal, and stops when
|
|
101
|
+
* nothing is, so a still app paints only when asked. The animation's first
|
|
102
|
+
* frame does not wait out an interval: at one frame every two seconds, that
|
|
103
|
+
* would hold its start off screen for two.
|
|
104
|
+
*/
|
|
105
|
+
animate(): Unsubscribe;
|
|
106
|
+
/**
|
|
107
|
+
* Hand the terminal to whatever launched the program, and take it back and
|
|
108
|
+
* repaint when that resumes it. What sends the app here is the app's own —
|
|
109
|
+
* in raw mode Ctrl+Z arrives as a key.
|
|
110
|
+
*/
|
|
111
|
+
suspend(): Promise<void>;
|
|
112
|
+
/**
|
|
113
|
+
* Hear each frame once it is on screen — what `frame` now returns. A
|
|
114
|
+
* handler that throws fails the app as a frame that throws does.
|
|
115
|
+
*/
|
|
116
|
+
onPaint(handler: (frame: readonly (readonly Segment[])[]) => void): Unsubscribe;
|
|
117
|
+
/** Hand the terminal back for good; `run` resolves. */
|
|
118
|
+
stop(): void;
|
|
119
|
+
/**
|
|
120
|
+
* Hand the terminal back for good; `run` rejects with `error`. For an error
|
|
121
|
+
* thrown where the app's own code runs outside a frame — a key handler, a
|
|
122
|
+
* timer — which would otherwise reach neither `run`'s caller nor the
|
|
123
|
+
* terminal's hand-back.
|
|
124
|
+
*/
|
|
125
|
+
fail(error: unknown): void;
|
|
126
|
+
private enter;
|
|
127
|
+
private leave;
|
|
128
|
+
private end;
|
|
129
|
+
private tick;
|
|
130
|
+
private paint;
|
|
131
|
+
private draw;
|
|
132
|
+
}
|
|
133
|
+
export {};
|
package/dist/host/app.js
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App — the runtime a terminal application runs in: it takes the terminal,
|
|
3
|
+
* paints one renderable on it every frame, and hands the terminal back on
|
|
4
|
+
* every path out.
|
|
5
|
+
*
|
|
6
|
+
* [LAW:single-enforcer] Every change this runtime makes to the terminal —
|
|
7
|
+
* the alternate screen, pointer reporting, the hidden cursor, raw mode — is
|
|
8
|
+
* made by `enter` and
|
|
9
|
+
* undone by `leave`, and nothing else touches them. Stopping, suspending, an
|
|
10
|
+
* error in a frame and the program ending by a signal or a crash all hand the
|
|
11
|
+
* terminal back through `leave`, so there is one list of what to undo and no
|
|
12
|
+
* exit path can hold a shorter one.
|
|
13
|
+
*
|
|
14
|
+
* [LAW:one-source-of-truth] The frame on screen is one value: the rows last
|
|
15
|
+
* painted, carrying on every cell where its owner drew it (`core/anchor`).
|
|
16
|
+
* Nothing about it is cached beside it, so a resize invalidates nothing — it
|
|
17
|
+
* is input to the next frame, which replaces the rows and their anchors at
|
|
18
|
+
* once. An event arriving before that repaint lands on the frame on screen,
|
|
19
|
+
* which is the one the user pointed at.
|
|
20
|
+
*
|
|
21
|
+
* [LAW:locality-or-seam] The terminal's bytes are identical under every host
|
|
22
|
+
* and live here: entering and leaving the alternate screen, the cursor, the
|
|
23
|
+
* frame's encoding. What differs per host — whether the program can end
|
|
24
|
+
* under us, how it is suspended — is the `TerminalHost`'s, so the same app
|
|
25
|
+
* runs in node and in a browser with no path of its own for either.
|
|
26
|
+
*/
|
|
27
|
+
import { frameRate, systemClock } from "../core/clock.js";
|
|
28
|
+
import { Console } from "../core/console.js";
|
|
29
|
+
import { Segment } from "../core/segment.js";
|
|
30
|
+
import { SurfacePainter } from "../core/paint.js";
|
|
31
|
+
import { fitHeight } from "../core/protocol.js";
|
|
32
|
+
import { hostEnvironment } from "./host-environment.js";
|
|
33
|
+
// [LAW:one-source-of-truth] How a frame draws — its glyphs, the style names
|
|
34
|
+
// it knows, what happens to a style it cannot resolve, markup and
|
|
35
|
+
// highlighting — is the console's to say, so App takes those options as the
|
|
36
|
+
// console declares them. This list is both the type and what is copied: the
|
|
37
|
+
// host alone decides the console's size, colours and sink, so no other
|
|
38
|
+
// property of the options object reaches it, whatever the object carries.
|
|
39
|
+
const DRAW_OPTIONS = ["asciiOnly", "theme", "onStyleError", "markup", "highlight", "highlighter"];
|
|
40
|
+
function drawOptions(options) {
|
|
41
|
+
const picked = {};
|
|
42
|
+
const copy = (key) => {
|
|
43
|
+
picked[key] = options[key];
|
|
44
|
+
};
|
|
45
|
+
DRAW_OPTIONS.forEach(copy);
|
|
46
|
+
return picked;
|
|
47
|
+
}
|
|
48
|
+
const DEFAULT_RATE = frameRate(30);
|
|
49
|
+
// Button presses, motion and the wheel, in the SGR encoding: coordinates as
|
|
50
|
+
// decimal numbers, so a terminal wider than 223 columns still reports them.
|
|
51
|
+
// The terminal reports a pointer by its row on the screen, and an inline
|
|
52
|
+
// frame does not know which row of the screen it starts on, so only the
|
|
53
|
+
// alternate surface gets them.
|
|
54
|
+
const POINTER = {
|
|
55
|
+
alternate: { on: "\x1b[?1006h\x1b[?1000h\x1b[?1003h", off: "\x1b[?1003l\x1b[?1000l\x1b[?1006l" },
|
|
56
|
+
inline: { on: "", off: "" },
|
|
57
|
+
};
|
|
58
|
+
export class App {
|
|
59
|
+
host;
|
|
60
|
+
surface;
|
|
61
|
+
view;
|
|
62
|
+
clock;
|
|
63
|
+
rate;
|
|
64
|
+
// What is animating now: one token per `animate()` not yet released.
|
|
65
|
+
animations = new Set();
|
|
66
|
+
stopTicking = null;
|
|
67
|
+
// [LAW:one-source-of-truth] The host is the console's whole environment:
|
|
68
|
+
// its size, its colours, where bytes go. Every frame renders with this
|
|
69
|
+
// console's options and is encoded for its destination.
|
|
70
|
+
console;
|
|
71
|
+
_phase = "idle";
|
|
72
|
+
_frame = [];
|
|
73
|
+
painter;
|
|
74
|
+
refreshQueued = false;
|
|
75
|
+
subscriptions = [];
|
|
76
|
+
paintHandlers = new Set();
|
|
77
|
+
settle = () => { };
|
|
78
|
+
constructor(options) {
|
|
79
|
+
this.host = options.host;
|
|
80
|
+
this.surface = options.surface;
|
|
81
|
+
this.painter = new SurfacePainter(options.surface, (bytes) => this.host.write(bytes));
|
|
82
|
+
this.view = options.view;
|
|
83
|
+
this.clock = options.clock ?? systemClock();
|
|
84
|
+
this.rate = options.rate ?? DEFAULT_RATE;
|
|
85
|
+
this.console = new Console({ ...drawOptions(options), environment: hostEnvironment(options.host) });
|
|
86
|
+
}
|
|
87
|
+
get phase() {
|
|
88
|
+
return this._phase;
|
|
89
|
+
}
|
|
90
|
+
/** The rows on screen now, as painted: what a pointer event lands on. */
|
|
91
|
+
get frame() {
|
|
92
|
+
return this._frame;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Take the terminal, paint the first frame, and resolve when the app stops
|
|
96
|
+
* — or reject with the error a frame threw or `fail` was given, the terminal
|
|
97
|
+
* handed back first.
|
|
98
|
+
* When the program ends under the app, `run` does not settle: nothing after
|
|
99
|
+
* it runs, as nothing after it would have without the app. An app runs once.
|
|
100
|
+
*/
|
|
101
|
+
run() {
|
|
102
|
+
if (this._phase !== "idle") {
|
|
103
|
+
return Promise.reject(new Error(`App.run: an app runs once, and this one is ${this._phase}`));
|
|
104
|
+
}
|
|
105
|
+
return new Promise((resolve, reject) => {
|
|
106
|
+
this.settle = (outcome) => {
|
|
107
|
+
switch (outcome.kind) {
|
|
108
|
+
case "stopped":
|
|
109
|
+
return resolve();
|
|
110
|
+
case "failed":
|
|
111
|
+
return reject(outcome.error);
|
|
112
|
+
// The rest of a crash — its report, then its exit — is still
|
|
113
|
+
// running, and code resumed after `run` would run inside it.
|
|
114
|
+
case "ended":
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
this._phase = "running";
|
|
119
|
+
this.host.start();
|
|
120
|
+
this.subscriptions = [
|
|
121
|
+
this.host.onResize(() => this.refresh()),
|
|
122
|
+
// The program is ending under the app — by a signal, a crash, or an
|
|
123
|
+
// exit the app's own code never saw. Ending hands the terminal back,
|
|
124
|
+
// and the program then ends as it was going to.
|
|
125
|
+
this.host.onExit(() => this.end({ kind: "ended" })),
|
|
126
|
+
];
|
|
127
|
+
this.enter();
|
|
128
|
+
this.paint();
|
|
129
|
+
this.tick();
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Paint a new frame from the view, once, in a later task: every change made
|
|
134
|
+
* before then lands in the one frame.
|
|
135
|
+
*
|
|
136
|
+
* [LAW:no-ambient-temporal-coupling] A task, not a microtask. Input is read
|
|
137
|
+
* between tasks, so a view whose every frame asks for another still hears
|
|
138
|
+
* its keys — Ctrl+C included — where a microtask chain would starve them
|
|
139
|
+
* for as long as it ran.
|
|
140
|
+
*/
|
|
141
|
+
refresh() {
|
|
142
|
+
if (this.refreshQueued)
|
|
143
|
+
return;
|
|
144
|
+
this.refreshQueued = true;
|
|
145
|
+
setTimeout(() => {
|
|
146
|
+
// A frame painted since it was asked for — a tick — drew every change
|
|
147
|
+
// made before it, so there is nothing left to paint.
|
|
148
|
+
if (!this.refreshQueued)
|
|
149
|
+
return;
|
|
150
|
+
this.refreshQueued = false;
|
|
151
|
+
// [LAW:types-are-the-program] Only a running app holds the terminal; a
|
|
152
|
+
// suspended one repaints when it resumes, and a stopped one never does.
|
|
153
|
+
if (this._phase === "running")
|
|
154
|
+
this.paint();
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Paint at the app's `rate` until the returned function is called: what an
|
|
159
|
+
* effect sampled at the frame's time asks for while it moves. The app ticks
|
|
160
|
+
* while anything is animating and it holds the terminal, and stops when
|
|
161
|
+
* nothing is, so a still app paints only when asked. The animation's first
|
|
162
|
+
* frame does not wait out an interval: at one frame every two seconds, that
|
|
163
|
+
* would hold its start off screen for two.
|
|
164
|
+
*/
|
|
165
|
+
animate() {
|
|
166
|
+
const animation = {};
|
|
167
|
+
this.animations.add(animation);
|
|
168
|
+
this.tick();
|
|
169
|
+
this.refresh();
|
|
170
|
+
return () => {
|
|
171
|
+
this.animations.delete(animation);
|
|
172
|
+
this.tick();
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Hand the terminal to whatever launched the program, and take it back and
|
|
177
|
+
* repaint when that resumes it. What sends the app here is the app's own —
|
|
178
|
+
* in raw mode Ctrl+Z arrives as a key.
|
|
179
|
+
*/
|
|
180
|
+
async suspend() {
|
|
181
|
+
if (this._phase !== "running")
|
|
182
|
+
return;
|
|
183
|
+
this._phase = "suspended";
|
|
184
|
+
this.tick();
|
|
185
|
+
this.leave();
|
|
186
|
+
await this.host.suspend();
|
|
187
|
+
// Stopped while suspended — by a signal, say: the terminal is already
|
|
188
|
+
// handed back for good.
|
|
189
|
+
if (this._phase !== "suspended")
|
|
190
|
+
return;
|
|
191
|
+
this._phase = "running";
|
|
192
|
+
this.enter();
|
|
193
|
+
this.paint();
|
|
194
|
+
this.tick();
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Hear each frame once it is on screen — what `frame` now returns. A
|
|
198
|
+
* handler that throws fails the app as a frame that throws does.
|
|
199
|
+
*/
|
|
200
|
+
onPaint(handler) {
|
|
201
|
+
this.paintHandlers.add(handler);
|
|
202
|
+
return () => this.paintHandlers.delete(handler);
|
|
203
|
+
}
|
|
204
|
+
/** Hand the terminal back for good; `run` resolves. */
|
|
205
|
+
stop() {
|
|
206
|
+
this.end({ kind: "stopped" });
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Hand the terminal back for good; `run` rejects with `error`. For an error
|
|
210
|
+
* thrown where the app's own code runs outside a frame — a key handler, a
|
|
211
|
+
* timer — which would otherwise reach neither `run`'s caller nor the
|
|
212
|
+
* terminal's hand-back.
|
|
213
|
+
*/
|
|
214
|
+
fail(error) {
|
|
215
|
+
this.end({ kind: "failed", error });
|
|
216
|
+
}
|
|
217
|
+
// --- the terminal ---
|
|
218
|
+
enter() {
|
|
219
|
+
this.host.setRawMode(true);
|
|
220
|
+
this.painter.take();
|
|
221
|
+
this.host.write(POINTER[this.surface].on);
|
|
222
|
+
}
|
|
223
|
+
leave() {
|
|
224
|
+
this.host.write(POINTER[this.surface].off);
|
|
225
|
+
this.painter.handBack();
|
|
226
|
+
this.host.setRawMode(false);
|
|
227
|
+
}
|
|
228
|
+
end(outcome) {
|
|
229
|
+
const phase = this._phase;
|
|
230
|
+
if (phase === "idle" || phase === "stopped")
|
|
231
|
+
return;
|
|
232
|
+
this._phase = "stopped";
|
|
233
|
+
this.tick();
|
|
234
|
+
for (const unsubscribe of this.subscriptions)
|
|
235
|
+
unsubscribe();
|
|
236
|
+
this.subscriptions = [];
|
|
237
|
+
// A suspended app handed the terminal back when it suspended.
|
|
238
|
+
if (phase === "running")
|
|
239
|
+
this.leave();
|
|
240
|
+
this.host.stop();
|
|
241
|
+
this.settle(outcome);
|
|
242
|
+
}
|
|
243
|
+
// [LAW:single-enforcer] Whether the clock is ticking follows from the
|
|
244
|
+
// phase and what is animating, and is brought in line with them here
|
|
245
|
+
// alone, after every change to either — so a timer runs exactly while a
|
|
246
|
+
// running app has something animating, and none outlives the app.
|
|
247
|
+
tick() {
|
|
248
|
+
const wanted = this._phase === "running" && this.animations.size > 0;
|
|
249
|
+
const ticking = this.stopTicking;
|
|
250
|
+
if (ticking === null) {
|
|
251
|
+
if (wanted)
|
|
252
|
+
this.stopTicking = this.clock.every(this.rate, () => this.paint());
|
|
253
|
+
}
|
|
254
|
+
else if (!wanted) {
|
|
255
|
+
ticking();
|
|
256
|
+
this.stopTicking = null;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
// [LAW:no-silent-failure] A frame that throws ends the app with that
|
|
260
|
+
// error: the terminal is handed back, and `run` rejects with it.
|
|
261
|
+
paint() {
|
|
262
|
+
try {
|
|
263
|
+
this.draw();
|
|
264
|
+
}
|
|
265
|
+
catch (error) {
|
|
266
|
+
this.end({ kind: "failed", error });
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
draw() {
|
|
270
|
+
// This frame draws every change made before it, a queued refresh's included.
|
|
271
|
+
this.refreshQueued = false;
|
|
272
|
+
const options = this.console.options;
|
|
273
|
+
const height = this.painter.height(options.height.rows);
|
|
274
|
+
// [LAW:no-ambient-temporal-coupling] The frame owner reads the clock,
|
|
275
|
+
// once a frame; the view is handed the time as data.
|
|
276
|
+
const lines = Segment.splitLines(this.view(this.clock.now()).render({ ...options, height }));
|
|
277
|
+
// The app set the budget, so the app shapes what comes back: no deeper
|
|
278
|
+
// than the terminal — a taller frame would scroll the rows the painter
|
|
279
|
+
// goes back over — and, as a region, exactly that deep.
|
|
280
|
+
const frame = this.painter.paint(() => fitHeight(lines.slice(0, height.rows), height), { rows: height.rows, cols: options.maxWidth }, this.console.destination);
|
|
281
|
+
this._frame = frame;
|
|
282
|
+
for (const handler of [...this.paintHandlers])
|
|
283
|
+
handler(frame);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hostEnvironment — a `TerminalHost` as the `ConsoleEnvironment` a `Console`
|
|
3
|
+
* consults, so a console writing through a host asks that host every question
|
|
4
|
+
* it has about its destination.
|
|
5
|
+
*
|
|
6
|
+
* [LAW:one-source-of-truth] A console asks its destination four things: where
|
|
7
|
+
* bytes go, is it a terminal, how big is it, and — through the environment —
|
|
8
|
+
* what colours it draws. A bare sink answers only the first, and the other
|
|
9
|
+
* three fall to the ambient `process`: some other stream's answer in node,
|
|
10
|
+
* none in a browser, where a console on xterm.js saw an unnamed TTY and drew
|
|
11
|
+
* 16 colours. Handing the console the whole environment keeps all four
|
|
12
|
+
* answers the host's.
|
|
13
|
+
*
|
|
14
|
+
* The host's size is `size()`, so the environment handed on carries no
|
|
15
|
+
* `COLUMNS` or `LINES`: a console reads those ahead of the stream, and an
|
|
16
|
+
* exported `COLUMNS` in the shell that launched a node host would pin the
|
|
17
|
+
* console's width while the terminal it draws on is resized. The stream's
|
|
18
|
+
* `columns` and `rows` are getters, so a console that was not given a fixed
|
|
19
|
+
* size follows the host through resizes.
|
|
20
|
+
*/
|
|
21
|
+
import type { ConsoleEnvironment, ConsoleStream } from "../core/console.js";
|
|
22
|
+
import type { TerminalHost } from "./terminal-host.js";
|
|
23
|
+
export declare function hostEnvironment(host: TerminalHost): ConsoleEnvironment & {
|
|
24
|
+
readonly stdout: ConsoleStream;
|
|
25
|
+
readonly stderr: ConsoleStream;
|
|
26
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hostEnvironment — a `TerminalHost` as the `ConsoleEnvironment` a `Console`
|
|
3
|
+
* consults, so a console writing through a host asks that host every question
|
|
4
|
+
* it has about its destination.
|
|
5
|
+
*
|
|
6
|
+
* [LAW:one-source-of-truth] A console asks its destination four things: where
|
|
7
|
+
* bytes go, is it a terminal, how big is it, and — through the environment —
|
|
8
|
+
* what colours it draws. A bare sink answers only the first, and the other
|
|
9
|
+
* three fall to the ambient `process`: some other stream's answer in node,
|
|
10
|
+
* none in a browser, where a console on xterm.js saw an unnamed TTY and drew
|
|
11
|
+
* 16 colours. Handing the console the whole environment keeps all four
|
|
12
|
+
* answers the host's.
|
|
13
|
+
*
|
|
14
|
+
* The host's size is `size()`, so the environment handed on carries no
|
|
15
|
+
* `COLUMNS` or `LINES`: a console reads those ahead of the stream, and an
|
|
16
|
+
* exported `COLUMNS` in the shell that launched a node host would pin the
|
|
17
|
+
* console's width while the terminal it draws on is resized. The stream's
|
|
18
|
+
* `columns` and `rows` are getters, so a console that was not given a fixed
|
|
19
|
+
* size follows the host through resizes.
|
|
20
|
+
*/
|
|
21
|
+
// [LAW:types-are-the-program] A host always has a stream, so the return type
|
|
22
|
+
// says so rather than inheriting `ConsoleEnvironment`'s optional ones. It is
|
|
23
|
+
// one terminal, so stdout and stderr are the same stream — which is where a
|
|
24
|
+
// program's two streams land when both are attached to a terminal.
|
|
25
|
+
export function hostEnvironment(host) {
|
|
26
|
+
const { COLUMNS: _columns, LINES: _lines, ...env } = host.env;
|
|
27
|
+
const stream = {
|
|
28
|
+
write(chunk) {
|
|
29
|
+
host.write(chunk);
|
|
30
|
+
},
|
|
31
|
+
// [LAW:one-source-of-truth] A console asks whether its output is a
|
|
32
|
+
// terminal, which is the host's `writesToTerminal`, not its `isTTY`.
|
|
33
|
+
get isTTY() {
|
|
34
|
+
return host.writesToTerminal;
|
|
35
|
+
},
|
|
36
|
+
get columns() {
|
|
37
|
+
return host.size().cols;
|
|
38
|
+
},
|
|
39
|
+
get rows() {
|
|
40
|
+
return host.size().rows;
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
return { env, stdout: stream, stderr: stream };
|
|
44
|
+
}
|