@promptctl/rich-js 0.17.0 → 0.18.1
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 +13 -26
- 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 +0 -1
- package/dist/core/box.js +6 -18
- package/dist/core/cells.d.ts +16 -5
- package/dist/core/cells.js +31 -10
- package/dist/core/color.d.ts +4 -8
- package/dist/core/color.js +28 -19
- package/dist/core/console.d.ts +13 -4
- package/dist/core/console.js +91 -36
- package/dist/core/export-html.d.ts +40 -5
- package/dist/core/export-html.js +111 -17
- package/dist/core/markup.d.ts +27 -5
- package/dist/core/markup.js +282 -170
- package/dist/core/measure.d.ts +1 -1
- package/dist/core/measure.js +2 -1
- package/dist/core/place.d.ts +30 -0
- package/dist/core/place.js +51 -0
- package/dist/core/pretty.d.ts +3 -0
- package/dist/core/pretty.js +5 -1
- package/dist/core/protocol.d.ts +136 -6
- package/dist/core/protocol.js +64 -2
- package/dist/core/render.d.ts +2 -2
- package/dist/core/render.js +2 -2
- package/dist/core/segment.d.ts +23 -4
- package/dist/core/segment.js +75 -16
- package/dist/core/spinnerData.d.ts +2 -0
- package/dist/core/spinnerData.js +3 -1
- package/dist/core/strip.d.ts +3 -7
- package/dist/core/strip.js +32 -35
- package/dist/core/style.d.ts +25 -0
- package/dist/core/style.js +61 -89
- package/dist/core/text.d.ts +16 -15
- package/dist/core/text.js +62 -76
- package/dist/host/app.d.ts +121 -0
- package/dist/host/app.js +255 -0
- package/dist/host/host-environment.d.ts +26 -0
- package/dist/host/host-environment.js +44 -0
- package/dist/host/index.d.ts +4 -2
- package/dist/host/index.js +2 -1
- package/dist/host/terminal-host.d.ts +43 -7
- package/dist/host/terminal-host.js +57 -3
- package/dist/index.d.ts +7 -3
- package/dist/index.js +4 -1
- package/dist/node/terminal-host.d.ts +31 -2
- package/dist/node/terminal-host.js +69 -2
- package/dist/node/traceback.d.ts +16 -6
- package/dist/node/traceback.js +30 -8
- package/dist/renderables/align.d.ts +2 -2
- package/dist/renderables/align.js +6 -19
- package/dist/renderables/columns.js +4 -20
- package/dist/renderables/embed.d.ts +50 -0
- package/dist/renderables/embed.js +71 -0
- package/dist/renderables/group.js +3 -1
- package/dist/renderables/layout.js +31 -34
- package/dist/renderables/live.d.ts +6 -3
- package/dist/renderables/live.js +89 -45
- package/dist/renderables/markdown.d.ts +1 -1
- package/dist/renderables/markdown.js +6 -4
- package/dist/renderables/padding.js +3 -2
- package/dist/renderables/panel.js +26 -41
- package/dist/renderables/progress.js +5 -8
- package/dist/renderables/progressBar.js +6 -5
- package/dist/renderables/rule.d.ts +3 -2
- package/dist/renderables/rule.js +9 -9
- package/dist/renderables/spinner.d.ts +5 -3
- package/dist/renderables/spinner.js +14 -8
- package/dist/renderables/syntax.d.ts +1 -1
- package/dist/renderables/syntax.js +2 -1
- package/dist/renderables/table.d.ts +26 -14
- package/dist/renderables/table.js +239 -141
- package/dist/renderables/traceback.js +8 -9
- package/dist/renderables/tree.d.ts +17 -3
- package/dist/renderables/tree.js +62 -71
- package/dist/renderables/viewport.d.ts +114 -0
- package/dist/renderables/viewport.js +198 -0
- package/dist/template-bindings/color-funcs.js +11 -13
- package/dist/template-bindings/index.js +4 -2
- package/dist/template-bindings/palette-funcs.js +6 -16
- package/dist/template-bindings/style-funcs.js +4 -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.js +19 -0
- package/dist/themes/data/atom-one-light.js +19 -0
- package/dist/themes/data/catppuccin-frappe.js +19 -0
- package/dist/themes/data/catppuccin-latte.js +19 -0
- package/dist/themes/data/catppuccin-macchiato.js +19 -0
- package/dist/themes/data/catppuccin-mocha.js +19 -0
- package/dist/themes/data/cyberpunk.js +2 -0
- package/dist/themes/data/default.js +2 -0
- package/dist/themes/data/dracula.js +19 -0
- package/dist/themes/data/flexoki.js +19 -0
- package/dist/themes/data/gruvbox.js +19 -0
- package/dist/themes/data/monokai.js +19 -0
- package/dist/themes/data/nord.js +19 -0
- package/dist/themes/data/rose-pine-dawn.js +19 -0
- package/dist/themes/data/rose-pine-moon.js +19 -0
- package/dist/themes/data/rose-pine.js +19 -0
- package/dist/themes/data/solarized-dark.js +19 -0
- package/dist/themes/data/solarized-light.js +19 -0
- package/dist/themes/data/svg-export.js +19 -0
- package/dist/themes/data/textual-ansi.js +2 -0
- package/dist/themes/data/textual-dark.js +2 -0
- package/dist/themes/data/textual-light.js +2 -0
- package/dist/themes/data/tokyo-night.js +19 -0
- package/dist/themes/data/types.d.ts +16 -0
- package/dist/themes/data/types.js +10 -1
- package/dist/themes/registry.d.ts +3 -2
- package/dist/themes/registry.js +13 -1
- package/dist/themes/terminalThemes.d.ts +2 -7
- package/dist/themes/terminalThemes.js +4 -9
- package/dist/widgets/button.d.ts +1 -1
- package/dist/widgets/button.js +4 -2
- package/dist/widgets/checkbox.d.ts +2 -2
- package/dist/widgets/checkbox.js +5 -3
- package/dist/widgets/dropdown.d.ts +2 -2
- package/dist/widgets/dropdown.js +13 -16
- package/dist/widgets/event-router.d.ts +21 -28
- package/dist/widgets/event-router.js +80 -73
- package/dist/widgets/focus-manager.d.ts +25 -6
- package/dist/widgets/focus-manager.js +63 -65
- package/dist/widgets/hit.d.ts +64 -0
- package/dist/widgets/hit.js +113 -0
- package/dist/widgets/index.d.ts +5 -7
- package/dist/widgets/index.js +3 -4
- package/dist/widgets/slider.d.ts +2 -2
- package/dist/widgets/slider.js +7 -7
- package/dist/widgets/static-item.d.ts +9 -15
- package/dist/widgets/static-item.js +9 -15
- package/dist/widgets/text-input.d.ts +2 -2
- package/dist/widgets/text-input.js +17 -14
- package/dist/widgets/toggle.d.ts +1 -1
- package/dist/widgets/toggle.js +3 -2
- package/dist/widgets/types.d.ts +14 -35
- package/dist/widgets/types.js +0 -1
- package/dist/widgets/widget-app.d.ts +63 -0
- package/dist/widgets/widget-app.js +199 -0
- package/dist/widgets/widget-base.d.ts +4 -6
- package/dist/widgets/widget-base.js +22 -30
- package/package.json +12 -4
- package/dist/host/host-stream.d.ts +0 -30
- package/dist/host/host-stream.js +0 -35
- package/dist/widgets/screen.d.ts +0 -98
- package/dist/widgets/screen.js +0 -530
|
@@ -3,25 +3,9 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { Segment } from "../core/segment.js";
|
|
5
5
|
import { Measurement } from "../core/measure.js";
|
|
6
|
-
import {
|
|
7
|
-
import { isMeasurable, withBoundedWidth, withCellWidth } from "../core/protocol.js";
|
|
6
|
+
import { isMeasurable, stackedHeight, withBoundedWidth, withCellWidth } from "../core/protocol.js";
|
|
8
7
|
import { cellCount } from "../core/cells.js";
|
|
9
|
-
|
|
10
|
-
// [LAW:single-enforcer] A `RichText` passed directly implements `render`
|
|
11
|
-
// and would otherwise leave through the passthrough arm below untouched,
|
|
12
|
-
// keeping its default `end: "\n"` (rich-text-5ai) — the same gap the code
|
|
13
|
-
// review found in table.ts's `toRenderable`. A column item is a row
|
|
14
|
-
// fragment, not a printed unit, same as every other embedding site.
|
|
15
|
-
if (item instanceof RichText) {
|
|
16
|
-
const copy = item.copy();
|
|
17
|
-
copy.end = "";
|
|
18
|
-
return copy;
|
|
19
|
-
}
|
|
20
|
-
if (typeof item === "object" && item !== null && "render" in item) {
|
|
21
|
-
return item;
|
|
22
|
-
}
|
|
23
|
-
return new RichText(String(item ?? ""), { end: "" });
|
|
24
|
-
}
|
|
8
|
+
import { embed } from "./embed.js";
|
|
25
9
|
export class Columns {
|
|
26
10
|
renderables;
|
|
27
11
|
expand;
|
|
@@ -30,7 +14,7 @@ export class Columns {
|
|
|
30
14
|
columnFirst;
|
|
31
15
|
gutterWidth;
|
|
32
16
|
constructor(items, options) {
|
|
33
|
-
this.renderables = items ? [...items].map(
|
|
17
|
+
this.renderables = items ? [...items].map(embed) : [];
|
|
34
18
|
this.expand = options?.expand ?? false;
|
|
35
19
|
this.equal = options?.equal ?? false;
|
|
36
20
|
// A declared column width is a cell count like any other, and it reaches
|
|
@@ -70,7 +54,7 @@ export class Columns {
|
|
|
70
54
|
const lines = item === undefined
|
|
71
55
|
? []
|
|
72
56
|
: Segment.splitLines([
|
|
73
|
-
...item.render({ ...options, maxWidth: width }),
|
|
57
|
+
...item.render({ ...options, maxWidth: width, height: stackedHeight(options.height) }),
|
|
74
58
|
]);
|
|
75
59
|
return { lines, width };
|
|
76
60
|
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one crossing where content a caller hands a renderable — a table cell, a
|
|
3
|
+
* panel's body, title or subtitle, a tree label, a column item, a layout pane,
|
|
4
|
+
* a rule's title — becomes something that renderable can lay out.
|
|
5
|
+
* [LAW:single-enforcer]
|
|
6
|
+
*
|
|
7
|
+
* Each of those sites used to build its own `RichText` straight from the
|
|
8
|
+
* constructor, which does not parse markup, so `[red]Solo[/red]` reached the
|
|
9
|
+
* terminal with its tags intact. Table cells were fixed on their own first; the
|
|
10
|
+
* rest kept the defect until the rule moved here.
|
|
11
|
+
*
|
|
12
|
+
* Markup is parsed because Rich parses it: every one of these positions
|
|
13
|
+
* reaches the wire through Rich's `render_str`, and a console's markup is on by
|
|
14
|
+
* default. Rich's `render_str` also honours `Console(markup=False)` and runs the
|
|
15
|
+
* console's highlighter; this crossing does neither, because the console's
|
|
16
|
+
* settings do not reach it (rich-markup-3sw).
|
|
17
|
+
*
|
|
18
|
+
* `end` is cleared because embedded text is a fragment rather than a printed
|
|
19
|
+
* line; left at the default `"\n"` it draws a trailing blank row. A `RichText`
|
|
20
|
+
* is copied first, since clearing in place would reach back into the caller's
|
|
21
|
+
* object.
|
|
22
|
+
*/
|
|
23
|
+
import type { Measurable, Renderable, RenderOptions } from "../core/protocol.js";
|
|
24
|
+
import { Segment } from "../core/segment.js";
|
|
25
|
+
import type { Style } from "../core/style.js";
|
|
26
|
+
import { RichText } from "../core/text.js";
|
|
27
|
+
/**
|
|
28
|
+
* Caller content as the text an embedding site lays out, `end` cleared. A
|
|
29
|
+
* string is the only kind of content that can contain markup, so it is the only
|
|
30
|
+
* kind parsed; any other value is its `String` form as written, so an object's
|
|
31
|
+
* `[object Object]` is not eaten as a tag.
|
|
32
|
+
*/
|
|
33
|
+
export declare function embeddedText(content: unknown): RichText;
|
|
34
|
+
/**
|
|
35
|
+
* Caller content as something an embedding site can render. A non-text
|
|
36
|
+
* `Renderable` (a nested `Panel` or `Table`) passes through: it carries no
|
|
37
|
+
* `end` to clear and no markup to parse. Everything else is `embeddedText`.
|
|
38
|
+
*/
|
|
39
|
+
export declare function embed(content: unknown): Renderable & Partial<Measurable>;
|
|
40
|
+
/**
|
|
41
|
+
* Caller content set into a line it shares with other drawing — a panel's
|
|
42
|
+
* title or subtitle in its border, a rule's title — as one line of segments:
|
|
43
|
+
* a space either side, its own styles over `base`. The caller cuts it to the
|
|
44
|
+
* room it has with `Segment.adjustLineLength`, which is cell-aware.
|
|
45
|
+
*
|
|
46
|
+
* Content with no text is no label at all, not two spaces: an absent title, an
|
|
47
|
+
* empty string, an empty `RichText` and markup that styles nothing all draw
|
|
48
|
+
* the plain line, as Rich's do (an empty `Text` is falsy there).
|
|
49
|
+
*/
|
|
50
|
+
export declare function inlineLabel(content: unknown, options: RenderOptions, base: Style | undefined): Segment[];
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one crossing where content a caller hands a renderable — a table cell, a
|
|
3
|
+
* panel's body, title or subtitle, a tree label, a column item, a layout pane,
|
|
4
|
+
* a rule's title — becomes something that renderable can lay out.
|
|
5
|
+
* [LAW:single-enforcer]
|
|
6
|
+
*
|
|
7
|
+
* Each of those sites used to build its own `RichText` straight from the
|
|
8
|
+
* constructor, which does not parse markup, so `[red]Solo[/red]` reached the
|
|
9
|
+
* terminal with its tags intact. Table cells were fixed on their own first; the
|
|
10
|
+
* rest kept the defect until the rule moved here.
|
|
11
|
+
*
|
|
12
|
+
* Markup is parsed because Rich parses it: every one of these positions
|
|
13
|
+
* reaches the wire through Rich's `render_str`, and a console's markup is on by
|
|
14
|
+
* default. Rich's `render_str` also honours `Console(markup=False)` and runs the
|
|
15
|
+
* console's highlighter; this crossing does neither, because the console's
|
|
16
|
+
* settings do not reach it (rich-markup-3sw).
|
|
17
|
+
*
|
|
18
|
+
* `end` is cleared because embedded text is a fragment rather than a printed
|
|
19
|
+
* line; left at the default `"\n"` it draws a trailing blank row. A `RichText`
|
|
20
|
+
* is copied first, since clearing in place would reach back into the caller's
|
|
21
|
+
* object.
|
|
22
|
+
*/
|
|
23
|
+
import { renderMarkup } from "../core/markup.js";
|
|
24
|
+
import { Segment } from "../core/segment.js";
|
|
25
|
+
import { RichText } from "../core/text.js";
|
|
26
|
+
/**
|
|
27
|
+
* Caller content as the text an embedding site lays out, `end` cleared. A
|
|
28
|
+
* string is the only kind of content that can contain markup, so it is the only
|
|
29
|
+
* kind parsed; any other value is its `String` form as written, so an object's
|
|
30
|
+
* `[object Object]` is not eaten as a tag.
|
|
31
|
+
*/
|
|
32
|
+
export function embeddedText(content) {
|
|
33
|
+
const text = content instanceof RichText
|
|
34
|
+
? content.copy()
|
|
35
|
+
: typeof content === "string"
|
|
36
|
+
? renderMarkup(content)
|
|
37
|
+
: new RichText(String(content ?? ""));
|
|
38
|
+
text.end = "";
|
|
39
|
+
return text;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Caller content as something an embedding site can render. A non-text
|
|
43
|
+
* `Renderable` (a nested `Panel` or `Table`) passes through: it carries no
|
|
44
|
+
* `end` to clear and no markup to parse. Everything else is `embeddedText`.
|
|
45
|
+
*/
|
|
46
|
+
export function embed(content) {
|
|
47
|
+
if (!(content instanceof RichText) && typeof content === "object" && content !== null && "render" in content) {
|
|
48
|
+
return content;
|
|
49
|
+
}
|
|
50
|
+
return embeddedText(content);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Caller content set into a line it shares with other drawing — a panel's
|
|
54
|
+
* title or subtitle in its border, a rule's title — as one line of segments:
|
|
55
|
+
* a space either side, its own styles over `base`. The caller cuts it to the
|
|
56
|
+
* room it has with `Segment.adjustLineLength`, which is cell-aware.
|
|
57
|
+
*
|
|
58
|
+
* Content with no text is no label at all, not two spaces: an absent title, an
|
|
59
|
+
* empty string, an empty `RichText` and markup that styles nothing all draw
|
|
60
|
+
* the plain line, as Rich's do (an empty `Text` is falsy there).
|
|
61
|
+
*/
|
|
62
|
+
export function inlineLabel(content, options, base) {
|
|
63
|
+
// The label leaves at its natural width, `"ignore"`, because the caller does
|
|
64
|
+
// the cutting; the label's own overflow method would cut it first, and to a
|
|
65
|
+
// width no one measured. `"ignore"` also leaves it unjustified, so its spaces
|
|
66
|
+
// stay where `pad` put them.
|
|
67
|
+
const bare = embeddedText(content);
|
|
68
|
+
const text = bare.plain === "" ? bare : bare.pad(1);
|
|
69
|
+
text.overflow = "ignore";
|
|
70
|
+
return [...Segment.applyStyle(text.render(options), base)];
|
|
71
|
+
}
|
|
@@ -2,14 +2,16 @@
|
|
|
2
2
|
* Group — renders multiple renderables in sequence.
|
|
3
3
|
* No visual chrome — purely a container.
|
|
4
4
|
*/
|
|
5
|
+
import { stackedHeight } from "../core/protocol.js";
|
|
5
6
|
export class Group {
|
|
6
7
|
renderables;
|
|
7
8
|
constructor(...renderables) {
|
|
8
9
|
this.renderables = renderables;
|
|
9
10
|
}
|
|
10
11
|
*render(options) {
|
|
12
|
+
const memberOptions = { ...options, height: stackedHeight(options.height) };
|
|
11
13
|
for (const renderable of this.renderables) {
|
|
12
|
-
yield* renderable.render(
|
|
14
|
+
yield* renderable.render(memberOptions);
|
|
13
15
|
}
|
|
14
16
|
}
|
|
15
17
|
}
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* Layout — divides the screen into rectangular regions.
|
|
3
3
|
*/
|
|
4
4
|
import { Segment } from "../core/segment.js";
|
|
5
|
-
import {
|
|
6
|
-
import { isMeasurable, withBoundedWidth, withCellWidth } from "../core/protocol.js";
|
|
5
|
+
import { embed } from "./embed.js";
|
|
6
|
+
import { fitHeight, isMeasurable, regionRows, stackedHeight, withBoundedWidth, withCellWidth, } from "../core/protocol.js";
|
|
7
7
|
import { Measurement } from "../core/measure.js";
|
|
8
8
|
import { cellCount } from "../core/cells.js";
|
|
9
9
|
/**
|
|
@@ -16,23 +16,18 @@ import { cellCount } from "../core/cells.js";
|
|
|
16
16
|
function growthRatio(ratio) {
|
|
17
17
|
return Number.isFinite(ratio) && ratio > 0 ? ratio : 0;
|
|
18
18
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const copy = renderable.copy();
|
|
32
|
-
copy.end = "";
|
|
33
|
-
return copy;
|
|
34
|
-
}
|
|
35
|
-
return renderable;
|
|
19
|
+
/**
|
|
20
|
+
* A pane rendered into a region of `rows` and held to exactly that many lines,
|
|
21
|
+
* or — with `rows` undefined — rendered under the layout's own budget, as a
|
|
22
|
+
* ceiling, at its natural height.
|
|
23
|
+
*
|
|
24
|
+
* [LAW:single-enforcer] The region's setter shapes it (`fitHeight`). Forwarded
|
|
25
|
+
* unshaped, a pane whose content ran short pulled every pane below it up, and
|
|
26
|
+
* one that ran long pushed them down.
|
|
27
|
+
*/
|
|
28
|
+
function paneLines(pane, options, rows) {
|
|
29
|
+
const height = rows === undefined ? stackedHeight(options.height) : { rows, exact: true };
|
|
30
|
+
return fitHeight(Segment.splitLines(pane.render({ ...options, height })), height);
|
|
36
31
|
}
|
|
37
32
|
export class Layout {
|
|
38
33
|
name;
|
|
@@ -45,7 +40,7 @@ export class Layout {
|
|
|
45
40
|
_splitDirection;
|
|
46
41
|
constructor(renderable, options) {
|
|
47
42
|
if (renderable !== undefined) {
|
|
48
|
-
this._renderable =
|
|
43
|
+
this._renderable = embed(renderable);
|
|
49
44
|
}
|
|
50
45
|
this.name = options?.name;
|
|
51
46
|
this.ratio = options?.ratio ?? 1;
|
|
@@ -110,7 +105,7 @@ export class Layout {
|
|
|
110
105
|
this._splitDirection = "row";
|
|
111
106
|
}
|
|
112
107
|
update(renderable) {
|
|
113
|
-
this._renderable =
|
|
108
|
+
this._renderable = embed(renderable);
|
|
114
109
|
}
|
|
115
110
|
getByName(name) {
|
|
116
111
|
if (this.name === name)
|
|
@@ -151,29 +146,31 @@ export class Layout {
|
|
|
151
146
|
}
|
|
152
147
|
}
|
|
153
148
|
*_renderColumn(children, options) {
|
|
154
|
-
//
|
|
155
|
-
|
|
156
|
-
|
|
149
|
+
// Each pane gets full width and its share of the region. With no region —
|
|
150
|
+
// a ceiling, or no budget at all — a pane's share is its declared `size`,
|
|
151
|
+
// and a pane without one takes its content's height.
|
|
152
|
+
const region = regionRows(options.height);
|
|
153
|
+
const shares = region === undefined
|
|
154
|
+
? children.map((child) => child.size)
|
|
155
|
+
: this._distributeSpace(children, region);
|
|
157
156
|
for (let i = 0; i < children.length; i++) {
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
height: heights[i],
|
|
163
|
-
};
|
|
164
|
-
yield* child.render(childOptions);
|
|
157
|
+
for (const line of paneLines(children[i], options, shares[i])) {
|
|
158
|
+
yield* line;
|
|
159
|
+
yield Segment.line();
|
|
160
|
+
}
|
|
165
161
|
}
|
|
166
162
|
}
|
|
167
163
|
*_renderRow(children, options) {
|
|
168
164
|
// Horizontal side-by-side: divide the requested width once, then merge
|
|
169
165
|
// child lines. The budget arrives parsed from `render` — unparsed, a NaN
|
|
170
166
|
// width made every share NaN and the merge threw `Invalid array length`.
|
|
167
|
+
// Every pane stands in the whole region, and with none the merge pads the
|
|
168
|
+
// row to its tallest pane.
|
|
171
169
|
const widths = this._distributeSpace(children, options.maxWidth);
|
|
170
|
+
const region = regionRows(options.height);
|
|
172
171
|
const cells = children.map((child, i) => ({
|
|
173
172
|
width: widths[i],
|
|
174
|
-
lines:
|
|
175
|
-
...child.render({ ...options, maxWidth: widths[i] }),
|
|
176
|
-
]),
|
|
173
|
+
lines: paneLines(child, { ...options, maxWidth: widths[i] }, region),
|
|
177
174
|
}));
|
|
178
175
|
yield* Segment.mergeHorizontal(cells);
|
|
179
176
|
}
|
|
@@ -6,10 +6,11 @@
|
|
|
6
6
|
* region. Good for spinners/progress bars below other output.
|
|
7
7
|
* - **Alt-screen** (`altScreen: true`): enters the alternate screen buffer
|
|
8
8
|
* on start, cursor-homes on each refresh (no clear flicker), and restores
|
|
9
|
-
* the original buffer on stop.
|
|
9
|
+
* the original buffer on stop. The screen is the frame's region, so a
|
|
10
|
+
* `Layout` fills it. Good for full-screen TUI apps.
|
|
10
11
|
*/
|
|
11
12
|
import { Console } from "../core/console.js";
|
|
12
|
-
import type
|
|
13
|
+
import { type Renderable } from "../core/protocol.js";
|
|
13
14
|
export interface LiveOptions {
|
|
14
15
|
refreshPerSecond?: number;
|
|
15
16
|
autoRefresh?: boolean;
|
|
@@ -42,6 +43,8 @@ export declare class Live {
|
|
|
42
43
|
refresh?: boolean;
|
|
43
44
|
}): void;
|
|
44
45
|
refresh(): void;
|
|
45
|
-
private
|
|
46
|
+
private _overflow;
|
|
47
|
+
private _erase;
|
|
48
|
+
private _release;
|
|
46
49
|
private _writeCursorControl;
|
|
47
50
|
}
|
package/dist/renderables/live.js
CHANGED
|
@@ -6,11 +6,13 @@
|
|
|
6
6
|
* region. Good for spinners/progress bars below other output.
|
|
7
7
|
* - **Alt-screen** (`altScreen: true`): enters the alternate screen buffer
|
|
8
8
|
* on start, cursor-homes on each refresh (no clear flicker), and restores
|
|
9
|
-
* the original buffer on stop.
|
|
9
|
+
* the original buffer on stop. The screen is the frame's region, so a
|
|
10
|
+
* `Layout` fills it. Good for full-screen TUI apps.
|
|
10
11
|
*/
|
|
11
12
|
import { Console } from "../core/console.js";
|
|
12
13
|
import { Segment } from "../core/segment.js";
|
|
13
14
|
import { segmentsToString } from "../core/render.js";
|
|
15
|
+
import { fitHeight } from "../core/protocol.js";
|
|
14
16
|
export class Live {
|
|
15
17
|
_renderable;
|
|
16
18
|
_console;
|
|
@@ -53,26 +55,37 @@ export class Live {
|
|
|
53
55
|
this._writeCursorControl(false);
|
|
54
56
|
if (this._autoRefresh) {
|
|
55
57
|
const interval = Math.floor(1000 / this._refreshPerSecond);
|
|
56
|
-
|
|
58
|
+
// No caller is on the stack to run `stop()` for a frame that throws
|
|
59
|
+
// here, and in Node the throw ends the process — so the terminal is
|
|
60
|
+
// handed back first, and the error then goes on to wherever an uncaught
|
|
61
|
+
// one goes.
|
|
62
|
+
this._timer = setInterval(() => {
|
|
63
|
+
try {
|
|
64
|
+
this.refresh();
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
this._release();
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
}, interval);
|
|
57
71
|
}
|
|
58
72
|
}
|
|
59
73
|
stop() {
|
|
60
74
|
if (!this._started)
|
|
61
75
|
return;
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
this.
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
76
|
+
// [LAW:no-silent-failure] A final frame that throws still propagates, and
|
|
77
|
+
// the terminal is handed back on the way out.
|
|
78
|
+
try {
|
|
79
|
+
if (this._transient) {
|
|
80
|
+
this._console.file.write(this._erase());
|
|
81
|
+
this._lastLineCount = 0;
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
this.refresh();
|
|
85
|
+
}
|
|
69
86
|
}
|
|
70
|
-
|
|
71
|
-
this.
|
|
72
|
-
}
|
|
73
|
-
this._writeCursorControl(true);
|
|
74
|
-
if (this._altScreen) {
|
|
75
|
-
this._console.file.write("\x1b[0m\x1b[?1049l"); // reset attrs + exit alt screen
|
|
87
|
+
finally {
|
|
88
|
+
this._release();
|
|
76
89
|
}
|
|
77
90
|
}
|
|
78
91
|
update(renderable, options) {
|
|
@@ -84,45 +97,76 @@ export class Live {
|
|
|
84
97
|
}
|
|
85
98
|
}
|
|
86
99
|
refresh() {
|
|
87
|
-
|
|
100
|
+
// A frame is drawn only while this Live holds the terminal, as in Rich:
|
|
101
|
+
// before `start()` or after the hand-back it would land on a screen that
|
|
102
|
+
// belongs to someone else — the main buffer, under the user's own output.
|
|
103
|
+
if (!this._started || !this._renderable)
|
|
88
104
|
return;
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const segments = [...this._renderable.render(this._console.options)];
|
|
99
|
-
const lines = Segment.splitLines(segments);
|
|
100
|
-
const maxHeight = this._console.height;
|
|
101
|
-
let displayLines = lines;
|
|
102
|
-
if (this._verticalOverflow !== "visible" && lines.length > maxHeight) {
|
|
103
|
-
displayLines = lines.slice(0, maxHeight);
|
|
104
|
-
if (this._verticalOverflow === "ellipsis" && displayLines.length > 0) {
|
|
105
|
-
displayLines[displayLines.length - 1] = [new Segment("...")];
|
|
106
|
-
}
|
|
107
|
-
}
|
|
105
|
+
// [LAW:dataflow-not-control-flow] Both modes render under the terminal's
|
|
106
|
+
// rows and differ only in what those rows are: the alternate screen is a
|
|
107
|
+
// region the frame stands in, and an inline frame keeps its natural height
|
|
108
|
+
// under them as a ceiling. Live set the budget, so Live shapes what comes
|
|
109
|
+
// back — its overflow policy, then `fitHeight`, which pads only a region.
|
|
110
|
+
const options = this._console.options;
|
|
111
|
+
const height = { rows: options.height.rows, exact: this._altScreen };
|
|
112
|
+
const lines = Segment.splitLines(this._renderable.render({ ...options, height }));
|
|
113
|
+
const displayLines = fitHeight(this._overflow(lines, height.rows), height);
|
|
108
114
|
// [LAW:single-enforcer] Per-line encoding routes through the same
|
|
109
115
|
// tree-coalescer `Console._writeSegments` uses, so Live frames coalesce
|
|
110
116
|
// adjacent same-style segments into shared SGR pairs on the wire and
|
|
111
117
|
// encode for the console's own destination.
|
|
118
|
+
// The alternate screen is not erased between frames — the cursor only goes
|
|
119
|
+
// home — so each of its rows is erased before it is drawn, or a shorter
|
|
120
|
+
// frame leaves the last one's rows and line tails showing. The erase comes
|
|
121
|
+
// first because the cursor is then at the row's start; after a row that
|
|
122
|
+
// fills the width it would take the last cell. An inline frame's rows are
|
|
123
|
+
// erased by `_erase`.
|
|
124
|
+
const lead = this._altScreen ? "\x1b[2K" : "";
|
|
112
125
|
const destination = this._console.destination;
|
|
113
126
|
const output = displayLines
|
|
114
|
-
.map((line) => segmentsToString(line, destination))
|
|
127
|
+
.map((line) => lead + segmentsToString(line, destination))
|
|
115
128
|
.join("\n");
|
|
116
|
-
|
|
117
|
-
|
|
129
|
+
// [LAW:effects-at-boundaries] The frame is fully rendered before a byte
|
|
130
|
+
// reaches the terminal, and whatever takes the last frame away goes out in
|
|
131
|
+
// the same write as the new one — so a render that throws leaves the last
|
|
132
|
+
// good frame showing. The first alternate-screen frame clears the buffer
|
|
133
|
+
// it walked into; after that the cursor only goes home.
|
|
134
|
+
const replace = this._altScreen ? (this._firstRefresh ? "\x1b[2J\x1b[H" : "\x1b[H") : this._erase();
|
|
135
|
+
// Inline, the newline leaves the cursor under the frame, where `_erase`
|
|
136
|
+
// counts up from. On the alternate screen the next frame starts from home,
|
|
137
|
+
// and a newline after a full-height frame's last row scrolls its first row
|
|
138
|
+
// off the top.
|
|
139
|
+
this._console.file.write(replace + (this._altScreen ? output : output + "\n"));
|
|
140
|
+
this._firstRefresh = false;
|
|
141
|
+
// What `_erase` erases: an inline frame's rows. An alternate-screen frame
|
|
142
|
+
// is erased by leaving the buffer.
|
|
143
|
+
this._lastLineCount = this._altScreen ? 0 : displayLines.length;
|
|
118
144
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
145
|
+
// Lines past `rows`: dropped, with the last kept row replaced by an ellipsis,
|
|
146
|
+
// or left for the terminal to scroll.
|
|
147
|
+
_overflow(lines, rows) {
|
|
148
|
+
if (this._verticalOverflow === "visible" || lines.length <= rows)
|
|
149
|
+
return lines;
|
|
150
|
+
const kept = lines.slice(0, rows);
|
|
151
|
+
if (this._verticalOverflow === "ellipsis" && kept.length > 0) {
|
|
152
|
+
kept[kept.length - 1] = [new Segment("...")];
|
|
153
|
+
}
|
|
154
|
+
return kept;
|
|
155
|
+
}
|
|
156
|
+
// Cursor-up and erase-line for each row of the last inline frame.
|
|
157
|
+
_erase() {
|
|
158
|
+
return "\x1b[1A\x1b[2K".repeat(this._lastLineCount);
|
|
159
|
+
}
|
|
160
|
+
// [LAW:single-enforcer] The one way the terminal is handed back — the timer
|
|
161
|
+
// stopped, the cursor shown, the alternate screen left — whether `stop()`
|
|
162
|
+
// ran or a frame threw with no caller to run it.
|
|
163
|
+
_release() {
|
|
164
|
+
this._started = false;
|
|
165
|
+
clearInterval(this._timer);
|
|
166
|
+
this._timer = undefined;
|
|
167
|
+
this._writeCursorControl(true);
|
|
168
|
+
if (this._altScreen) {
|
|
169
|
+
this._console.file.write("\x1b[0m\x1b[?1049l"); // reset attrs + exit alt screen
|
|
126
170
|
}
|
|
127
171
|
}
|
|
128
172
|
_writeCursorControl(show) {
|
|
@@ -16,7 +16,7 @@ export declare class Markdown implements Renderable, Measurable {
|
|
|
16
16
|
readonly inlineCodeStyle: string | Style;
|
|
17
17
|
readonly hyperlinks: boolean;
|
|
18
18
|
constructor(markdown: string, options?: MarkdownOptions);
|
|
19
|
-
render(
|
|
19
|
+
render(rawOptions: RenderOptions): Iterable<Segment>;
|
|
20
20
|
measure(options: RenderOptions): {
|
|
21
21
|
minimum: number;
|
|
22
22
|
maximum: number;
|
|
@@ -6,7 +6,7 @@ import { Segment } from "../core/segment.js";
|
|
|
6
6
|
import { Style } from "../core/style.js";
|
|
7
7
|
import { RichText } from "../core/text.js";
|
|
8
8
|
import { Rule } from "./rule.js";
|
|
9
|
-
import { getStyle } from "../core/protocol.js";
|
|
9
|
+
import { drawable, getStyle, stackedHeight } from "../core/protocol.js";
|
|
10
10
|
function tokenize(markdown) {
|
|
11
11
|
const lines = markdown.split("\n");
|
|
12
12
|
const tokens = [];
|
|
@@ -124,7 +124,9 @@ export class Markdown {
|
|
|
124
124
|
this.inlineCodeStyle = options?.inlineCodeStyle ?? "markdown.code";
|
|
125
125
|
this.hyperlinks = options?.hyperlinks !== false;
|
|
126
126
|
}
|
|
127
|
-
*render(
|
|
127
|
+
*render(rawOptions) {
|
|
128
|
+
// Every block below is one of a stack.
|
|
129
|
+
const options = { ...rawOptions, height: stackedHeight(rawOptions.height) };
|
|
128
130
|
const tokens = tokenize(this.markdown);
|
|
129
131
|
for (const token of tokens) {
|
|
130
132
|
switch (token.type) {
|
|
@@ -156,7 +158,7 @@ export class Markdown {
|
|
|
156
158
|
break;
|
|
157
159
|
}
|
|
158
160
|
case "list_item": {
|
|
159
|
-
const bullet = token.ordered ? `${token.index}. ` : " • ";
|
|
161
|
+
const bullet = token.ordered ? `${token.index}. ` : drawable(options, " • ", " * ");
|
|
160
162
|
yield new Segment(bullet);
|
|
161
163
|
const text = applyInlineStyles(token.text);
|
|
162
164
|
yield* text.render({ ...options, maxWidth: options.maxWidth - bullet.length });
|
|
@@ -164,7 +166,7 @@ export class Markdown {
|
|
|
164
166
|
}
|
|
165
167
|
case "blockquote": {
|
|
166
168
|
const quoteStyle = Style.parse("dim italic");
|
|
167
|
-
yield new Segment("▎ ", getStyle(options, "markdown.hr"));
|
|
169
|
+
yield new Segment(drawable(options, "▎ ", "| "), getStyle(options, "markdown.hr"));
|
|
168
170
|
const text = applyInlineStyles(token.text);
|
|
169
171
|
yield* Segment.applyStyle([...text.render(options)], quoteStyle);
|
|
170
172
|
yield Segment.line();
|
|
@@ -5,7 +5,7 @@ import { Segment } from "../core/segment.js";
|
|
|
5
5
|
import { cellCount } from "../core/cells.js";
|
|
6
6
|
import { NULL_STYLE } from "../core/style.js";
|
|
7
7
|
import { Measurement } from "../core/measure.js";
|
|
8
|
-
import { getStyle, isMeasurable, withBoundedWidth, withCellWidth } from "../core/protocol.js";
|
|
8
|
+
import { fitHeight, getStyle, insetHeight, isMeasurable, withBoundedWidth, withCellWidth } from "../core/protocol.js";
|
|
9
9
|
/**
|
|
10
10
|
* Parses the shapes a caller may write padding in — one number, a vertical/
|
|
11
11
|
* horizontal pair, or all four sides — into the four-sided form every
|
|
@@ -91,9 +91,10 @@ export class Padding {
|
|
|
91
91
|
const innerOptions = {
|
|
92
92
|
...options,
|
|
93
93
|
maxWidth: geometry.contentWidth,
|
|
94
|
+
height: insetHeight(options.height, this.top + this.bottom),
|
|
94
95
|
};
|
|
95
96
|
const segments = [...this.renderable.render(innerOptions)];
|
|
96
|
-
const lines = Segment.splitLines(segments);
|
|
97
|
+
const lines = fitHeight(Segment.splitLines(segments), innerOptions.height);
|
|
97
98
|
const resolved = getStyle(options, this.style);
|
|
98
99
|
const style = resolved.isNull ? undefined : resolved;
|
|
99
100
|
// Zero-length spans need no branch to suppress: the wire boundary drops
|