@profullstack/hqtui 0.2.0 → 0.4.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/dist/ansi.d.ts +10 -0
- package/dist/ansi.d.ts.map +1 -1
- package/dist/ansi.js +18 -0
- package/dist/ansi.js.map +1 -1
- package/dist/app.d.ts +30 -0
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +95 -12
- package/dist/app.js.map +1 -1
- package/dist/cli.js +1 -1
- package/dist/diff.d.ts +37 -1
- package/dist/diff.d.ts.map +1 -1
- package/dist/diff.js +79 -4
- package/dist/diff.js.map +1 -1
- package/dist/graphics/braille.d.ts.map +1 -1
- package/dist/graphics/braille.js +15 -2
- package/dist/graphics/braille.js.map +1 -1
- package/dist/graphics/canvas.d.ts +76 -0
- package/dist/graphics/canvas.d.ts.map +1 -0
- package/dist/graphics/canvas.js +110 -0
- package/dist/graphics/canvas.js.map +1 -0
- package/dist/graphics/chart.d.ts +70 -0
- package/dist/graphics/chart.d.ts.map +1 -0
- package/dist/graphics/chart.js +249 -0
- package/dist/graphics/chart.js.map +1 -0
- package/dist/graphics/index.d.ts +3 -0
- package/dist/graphics/index.d.ts.map +1 -1
- package/dist/graphics/index.js +3 -0
- package/dist/graphics/index.js.map +1 -1
- package/dist/graphics/world-data.d.ts +22 -0
- package/dist/graphics/world-data.d.ts.map +1 -0
- package/dist/graphics/world-data.js +1300 -0
- package/dist/graphics/world-data.js.map +1 -0
- package/dist/graphics/world.d.ts +70 -0
- package/dist/graphics/world.d.ts.map +1 -0
- package/dist/graphics/world.js +137 -0
- package/dist/graphics/world.js.map +1 -0
- package/dist/index.d.ts +5 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -4
- package/dist/index.js.map +1 -1
- package/dist/layout.d.ts +21 -1
- package/dist/layout.d.ts.map +1 -1
- package/dist/layout.js +54 -4
- package/dist/layout.js.map +1 -1
- package/dist/richtext.d.ts +79 -0
- package/dist/richtext.d.ts.map +1 -0
- package/dist/richtext.js +272 -0
- package/dist/richtext.js.map +1 -0
- package/dist/surface.d.ts +37 -0
- package/dist/surface.d.ts.map +1 -1
- package/dist/surface.js +123 -15
- package/dist/surface.js.map +1 -1
- package/dist/terminal.d.ts +81 -0
- package/dist/terminal.d.ts.map +1 -1
- package/dist/terminal.js +131 -4
- package/dist/terminal.js.map +1 -1
- package/dist/ui.d.ts +108 -5
- package/dist/ui.d.ts.map +1 -1
- package/dist/ui.js +134 -11
- package/dist/ui.js.map +1 -1
- package/dist/unicode.d.ts +9 -0
- package/dist/unicode.d.ts.map +1 -1
- package/dist/unicode.js +25 -0
- package/dist/unicode.js.map +1 -1
- package/dist/widgets/calendar.d.ts +67 -0
- package/dist/widgets/calendar.d.ts.map +1 -0
- package/dist/widgets/calendar.js +118 -0
- package/dist/widgets/calendar.js.map +1 -0
- package/dist/widgets/chart.d.ts +24 -0
- package/dist/widgets/chart.d.ts.map +1 -0
- package/dist/widgets/chart.js +80 -0
- package/dist/widgets/chart.js.map +1 -0
- package/dist/widgets/index.d.ts +6 -0
- package/dist/widgets/index.d.ts.map +1 -1
- package/dist/widgets/index.js +6 -0
- package/dist/widgets/index.js.map +1 -1
- package/dist/widgets/meters.d.ts.map +1 -1
- package/dist/widgets/meters.js +11 -1
- package/dist/widgets/meters.js.map +1 -1
- package/dist/widgets/scrollbar.d.ts +66 -0
- package/dist/widgets/scrollbar.d.ts.map +1 -0
- package/dist/widgets/scrollbar.js +89 -0
- package/dist/widgets/scrollbar.js.map +1 -0
- package/dist/widgets/shadow.d.ts +42 -0
- package/dist/widgets/shadow.d.ts.map +1 -0
- package/dist/widgets/shadow.js +73 -0
- package/dist/widgets/shadow.js.map +1 -0
- package/dist/widgets/surface.d.ts +33 -0
- package/dist/widgets/surface.d.ts.map +1 -0
- package/dist/widgets/surface.js +37 -0
- package/dist/widgets/surface.js.map +1 -0
- package/dist/widgets/table.d.ts +1 -2
- package/dist/widgets/table.d.ts.map +1 -1
- package/dist/widgets/table.js +4 -12
- package/dist/widgets/table.js.map +1 -1
- package/dist/widgets/text.d.ts +12 -1
- package/dist/widgets/text.d.ts.map +1 -1
- package/dist/widgets/text.js +27 -3
- package/dist/widgets/text.js.map +1 -1
- package/dist/widgets/world.d.ts +32 -0
- package/dist/widgets/world.d.ts.map +1 -0
- package/dist/widgets/world.js +31 -0
- package/dist/widgets/world.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Styled text: more than one colour on a line.
|
|
3
|
+
*
|
|
4
|
+
* A `Span` is a run of text with its own style; a line is a list of spans; text
|
|
5
|
+
* is a list of lines. Every widget that took a `string` takes `RichText` too,
|
|
6
|
+
* and a bare string is simply the one-span case, so nothing that worked before
|
|
7
|
+
* has to change.
|
|
8
|
+
*
|
|
9
|
+
* The measuring and cutting here mirrors `unicode.ts` exactly rather than
|
|
10
|
+
* approximating it. `truncateSpans`, `fitSpans` and `wrapSpans` produce the
|
|
11
|
+
* same columns as `truncate`, `fit` and `wrap` for single-span input — there
|
|
12
|
+
* are tests that assert precisely that over a corpus, because two text layout
|
|
13
|
+
* engines that disagree by one column is a bug you find in a screenshot six
|
|
14
|
+
* months later.
|
|
15
|
+
*/
|
|
16
|
+
import { type Style } from "./buffer.ts";
|
|
17
|
+
import type { Color } from "./color.ts";
|
|
18
|
+
export interface Span {
|
|
19
|
+
text: string;
|
|
20
|
+
fg?: Color;
|
|
21
|
+
bg?: Color;
|
|
22
|
+
attrs?: number;
|
|
23
|
+
bold?: boolean;
|
|
24
|
+
dim?: boolean;
|
|
25
|
+
italic?: boolean;
|
|
26
|
+
underline?: boolean;
|
|
27
|
+
}
|
|
28
|
+
/** One line of styled text. */
|
|
29
|
+
export type SpanLine = Span[];
|
|
30
|
+
/**
|
|
31
|
+
* Anything a widget will accept where it used to take a string.
|
|
32
|
+
*
|
|
33
|
+
* A plain string is one unstyled span. `Span[]` is one line. `Span[][]` is
|
|
34
|
+
* several. Embedded newlines split into lines wherever they appear, so a span
|
|
35
|
+
* carrying "a\nb" behaves like the string "a\nb" always did.
|
|
36
|
+
*/
|
|
37
|
+
export type RichText = string | SpanLine | SpanLine[];
|
|
38
|
+
/** True when the value carries styling rather than being a bare string. */
|
|
39
|
+
export declare function isRich(value: RichText): value is SpanLine | SpanLine[];
|
|
40
|
+
/** The style a span paints with, over whatever the widget already decided. */
|
|
41
|
+
export declare function spanStyle(span: Span, base?: Style): Style;
|
|
42
|
+
/**
|
|
43
|
+
* Normalise any accepted shape into lines of spans, splitting on newlines.
|
|
44
|
+
*
|
|
45
|
+
* Empty spans are dropped, but a line that ends up with no spans is kept: a
|
|
46
|
+
* blank line between two paragraphs is content, not absence.
|
|
47
|
+
*/
|
|
48
|
+
export declare function toSpanLines(value: RichText): SpanLine[];
|
|
49
|
+
/** Display width of a line in terminal columns. */
|
|
50
|
+
export declare function spanWidth(line: SpanLine): number;
|
|
51
|
+
/** Concatenated text of a line, with styling discarded. */
|
|
52
|
+
export declare function spanText(line: SpanLine): string;
|
|
53
|
+
/**
|
|
54
|
+
* Truncate a line to `max` columns, appending `ellipsis` when it does not fit.
|
|
55
|
+
*
|
|
56
|
+
* The ellipsis takes the style of the span it interrupts, so cutting a red
|
|
57
|
+
* error message short leaves a red ellipsis rather than a stray default-coloured
|
|
58
|
+
* one.
|
|
59
|
+
*/
|
|
60
|
+
export declare function truncateSpans(line: SpanLine, max: number, ellipsis?: string): SpanLine;
|
|
61
|
+
/** Pad or truncate a line to exactly `width` columns. */
|
|
62
|
+
/**
|
|
63
|
+
* A span line with its first `columns` display columns removed.
|
|
64
|
+
*
|
|
65
|
+
* The string form of this is `dropColumns`; a styled line has to drop the same
|
|
66
|
+
* columns without losing the styles of the runs that survive, so it walks the
|
|
67
|
+
* spans and cuts inside whichever one straddles the offset.
|
|
68
|
+
*/
|
|
69
|
+
export declare function dropSpanColumns(line: SpanLine, columns: number): SpanLine;
|
|
70
|
+
export declare function fitSpans(line: SpanLine, width: number, align?: "left" | "right" | "center"): SpanLine;
|
|
71
|
+
/**
|
|
72
|
+
* Greedy word wrap at `width` columns, preserving each span's style across the
|
|
73
|
+
* break. Mirrors `wrap()` in unicode.ts, including its hard split of a word
|
|
74
|
+
* longer than the line and its trimming of the soft-wrapped line end.
|
|
75
|
+
*/
|
|
76
|
+
export declare function wrapSpans(line: SpanLine, width: number): SpanLine[];
|
|
77
|
+
/** Wrap every line of a rich value, in order. */
|
|
78
|
+
export declare function wrapRich(value: RichText, width: number): SpanLine[];
|
|
79
|
+
//# sourceMappingURL=richtext.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"richtext.d.ts","sourceRoot":"","sources":["../src/richtext.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,EAAQ,KAAK,KAAK,EAAE,MAAM,aAAa,CAAC;AAC/C,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAGxC,MAAM,WAAW,IAAI;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,CAAC,EAAE,KAAK,CAAC;IACX,EAAE,CAAC,EAAE,KAAK,CAAC;IACX,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,+BAA+B;AAC/B,MAAM,MAAM,QAAQ,GAAG,IAAI,EAAE,CAAC;AAE9B;;;;;;GAMG;AACH,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,QAAQ,GAAG,QAAQ,EAAE,CAAC;AAEtD,2EAA2E;AAC3E,wBAAgB,MAAM,CAAC,KAAK,EAAE,QAAQ,GAAG,KAAK,IAAI,QAAQ,GAAG,QAAQ,EAAE,CAEtE;AAMD,8EAA8E;AAC9E,wBAAgB,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,GAAE,KAAU,GAAG,KAAK,CAW7D;AAQD;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,QAAQ,GAAG,QAAQ,EAAE,CAoBvD;AAMD,mDAAmD;AACnD,wBAAgB,SAAS,CAAC,IAAI,EAAE,QAAQ,GAAG,MAAM,CAIhD;AAED,2DAA2D;AAC3D,wBAAgB,QAAQ,CAAC,IAAI,EAAE,QAAQ,GAAG,MAAM,CAI/C;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,EAAE,QAAQ,SAAM,GAAG,QAAQ,CAuBnF;AAED,yDAAyD;AACzD;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,GAAG,QAAQ,CAmBzE;AAED,wBAAgB,QAAQ,CACtB,IAAI,EAAE,QAAQ,EACd,KAAK,EAAE,MAAM,EACb,KAAK,GAAE,MAAM,GAAG,OAAO,GAAG,QAAiB,GAC1C,QAAQ,CAaV;AAmDD;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,GAAG,QAAQ,EAAE,CAmCnE;AAED,iDAAiD;AACjD,wBAAgB,QAAQ,CAAC,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,GAAG,QAAQ,EAAE,CAWnE"}
|
package/dist/richtext.js
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Styled text: more than one colour on a line.
|
|
3
|
+
*
|
|
4
|
+
* A `Span` is a run of text with its own style; a line is a list of spans; text
|
|
5
|
+
* is a list of lines. Every widget that took a `string` takes `RichText` too,
|
|
6
|
+
* and a bare string is simply the one-span case, so nothing that worked before
|
|
7
|
+
* has to change.
|
|
8
|
+
*
|
|
9
|
+
* The measuring and cutting here mirrors `unicode.ts` exactly rather than
|
|
10
|
+
* approximating it. `truncateSpans`, `fitSpans` and `wrapSpans` produce the
|
|
11
|
+
* same columns as `truncate`, `fit` and `wrap` for single-span input — there
|
|
12
|
+
* are tests that assert precisely that over a corpus, because two text layout
|
|
13
|
+
* engines that disagree by one column is a bug you find in a screenshot six
|
|
14
|
+
* months later.
|
|
15
|
+
*/
|
|
16
|
+
import { Attr } from "./buffer.js";
|
|
17
|
+
import { cellText, dropColumns, graphemes, stringWidth } from "./unicode.js";
|
|
18
|
+
/** True when the value carries styling rather than being a bare string. */
|
|
19
|
+
export function isRich(value) {
|
|
20
|
+
return typeof value !== "string";
|
|
21
|
+
}
|
|
22
|
+
function isLines(value) {
|
|
23
|
+
return value.length === 0 || Array.isArray(value[0]);
|
|
24
|
+
}
|
|
25
|
+
/** The style a span paints with, over whatever the widget already decided. */
|
|
26
|
+
export function spanStyle(span, base = {}) {
|
|
27
|
+
let attrs = span.attrs ?? base.attrs ?? 0;
|
|
28
|
+
if (span.bold)
|
|
29
|
+
attrs |= Attr.Bold;
|
|
30
|
+
if (span.dim)
|
|
31
|
+
attrs |= Attr.Dim;
|
|
32
|
+
if (span.italic)
|
|
33
|
+
attrs |= Attr.Italic;
|
|
34
|
+
if (span.underline)
|
|
35
|
+
attrs |= Attr.Underline;
|
|
36
|
+
return {
|
|
37
|
+
fg: span.fg ?? base.fg,
|
|
38
|
+
bg: span.bg ?? base.bg,
|
|
39
|
+
attrs,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/** Everything except the text, for cloning a span's styling onto new text. */
|
|
43
|
+
function styleOf(span) {
|
|
44
|
+
const { text: _text, ...rest } = span;
|
|
45
|
+
return rest;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Normalise any accepted shape into lines of spans, splitting on newlines.
|
|
49
|
+
*
|
|
50
|
+
* Empty spans are dropped, but a line that ends up with no spans is kept: a
|
|
51
|
+
* blank line between two paragraphs is content, not absence.
|
|
52
|
+
*/
|
|
53
|
+
export function toSpanLines(value) {
|
|
54
|
+
if (typeof value === "string") {
|
|
55
|
+
return value.split("\n").map((line) => (line === "" ? [] : [{ text: line }]));
|
|
56
|
+
}
|
|
57
|
+
const lines = isLines(value) ? value : [value];
|
|
58
|
+
const out = [];
|
|
59
|
+
for (const line of lines) {
|
|
60
|
+
let current = [];
|
|
61
|
+
for (const span of line) {
|
|
62
|
+
if (span.text === "")
|
|
63
|
+
continue;
|
|
64
|
+
const parts = span.text.split("\n");
|
|
65
|
+
current.push({ ...styleOf(span), text: parts[0] });
|
|
66
|
+
for (let i = 1; i < parts.length; i++) {
|
|
67
|
+
out.push(dropEmpty(current));
|
|
68
|
+
current = [{ ...styleOf(span), text: parts[i] }];
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
out.push(dropEmpty(current));
|
|
72
|
+
}
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
function dropEmpty(line) {
|
|
76
|
+
return line.filter((s) => s.text !== "");
|
|
77
|
+
}
|
|
78
|
+
/** Display width of a line in terminal columns. */
|
|
79
|
+
export function spanWidth(line) {
|
|
80
|
+
let w = 0;
|
|
81
|
+
for (const span of line)
|
|
82
|
+
w += stringWidth(span.text);
|
|
83
|
+
return w;
|
|
84
|
+
}
|
|
85
|
+
/** Concatenated text of a line, with styling discarded. */
|
|
86
|
+
export function spanText(line) {
|
|
87
|
+
let out = "";
|
|
88
|
+
for (const span of line)
|
|
89
|
+
out += span.text;
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Truncate a line to `max` columns, appending `ellipsis` when it does not fit.
|
|
94
|
+
*
|
|
95
|
+
* The ellipsis takes the style of the span it interrupts, so cutting a red
|
|
96
|
+
* error message short leaves a red ellipsis rather than a stray default-coloured
|
|
97
|
+
* one.
|
|
98
|
+
*/
|
|
99
|
+
export function truncateSpans(line, max, ellipsis = "…") {
|
|
100
|
+
if (max <= 0)
|
|
101
|
+
return [];
|
|
102
|
+
if (spanWidth(line) <= max)
|
|
103
|
+
return line;
|
|
104
|
+
const limit = Math.max(0, max - stringWidth(ellipsis));
|
|
105
|
+
const out = [];
|
|
106
|
+
let w = 0;
|
|
107
|
+
let last;
|
|
108
|
+
for (const span of line) {
|
|
109
|
+
let text = "";
|
|
110
|
+
for (const g of graphemes(span.text)) {
|
|
111
|
+
if (w + g.width > limit)
|
|
112
|
+
break;
|
|
113
|
+
text += cellText(g.value);
|
|
114
|
+
w += g.width;
|
|
115
|
+
}
|
|
116
|
+
if (text !== "") {
|
|
117
|
+
last = span;
|
|
118
|
+
out.push({ ...styleOf(span), text });
|
|
119
|
+
}
|
|
120
|
+
if (w >= limit)
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
const source = last ?? line[0];
|
|
124
|
+
out.push({ ...(source ? styleOf(source) : {}), text: ellipsis });
|
|
125
|
+
return out;
|
|
126
|
+
}
|
|
127
|
+
/** Pad or truncate a line to exactly `width` columns. */
|
|
128
|
+
/**
|
|
129
|
+
* A span line with its first `columns` display columns removed.
|
|
130
|
+
*
|
|
131
|
+
* The string form of this is `dropColumns`; a styled line has to drop the same
|
|
132
|
+
* columns without losing the styles of the runs that survive, so it walks the
|
|
133
|
+
* spans and cuts inside whichever one straddles the offset.
|
|
134
|
+
*/
|
|
135
|
+
export function dropSpanColumns(line, columns) {
|
|
136
|
+
if (columns <= 0)
|
|
137
|
+
return line;
|
|
138
|
+
const out = [];
|
|
139
|
+
let skipped = 0;
|
|
140
|
+
for (const span of line) {
|
|
141
|
+
const width = stringWidth(span.text);
|
|
142
|
+
if (skipped >= columns) {
|
|
143
|
+
out.push(span);
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (skipped + width <= columns) {
|
|
147
|
+
skipped += width;
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
// The cut lands inside this span: keep its style, drop its first columns.
|
|
151
|
+
out.push({ ...span, text: dropColumns(span.text, columns - skipped) });
|
|
152
|
+
skipped = columns;
|
|
153
|
+
}
|
|
154
|
+
return out;
|
|
155
|
+
}
|
|
156
|
+
export function fitSpans(line, width, align = "left") {
|
|
157
|
+
const cut = truncateSpans(line, width);
|
|
158
|
+
const pad = width - spanWidth(cut);
|
|
159
|
+
if (pad <= 0)
|
|
160
|
+
return cut;
|
|
161
|
+
// Padding carries no style of its own so it picks up the widget's, which is
|
|
162
|
+
// what the string path has always done by padding before it draws.
|
|
163
|
+
const space = (n) => (n > 0 ? [{ text: " ".repeat(n) }] : []);
|
|
164
|
+
if (align === "right")
|
|
165
|
+
return [...space(pad), ...cut];
|
|
166
|
+
if (align === "center") {
|
|
167
|
+
const left = Math.floor(pad / 2);
|
|
168
|
+
return [...space(left), ...cut, ...space(pad - left)];
|
|
169
|
+
}
|
|
170
|
+
return [...cut, ...space(pad)];
|
|
171
|
+
}
|
|
172
|
+
/** Split a line into whitespace and non-whitespace runs, keeping each style. */
|
|
173
|
+
function tokenize(line) {
|
|
174
|
+
const out = [];
|
|
175
|
+
for (const span of line) {
|
|
176
|
+
const style = styleOf(span);
|
|
177
|
+
for (const part of span.text.split(/(\s+)/)) {
|
|
178
|
+
if (part === "")
|
|
179
|
+
continue;
|
|
180
|
+
out.push({ text: part, style, space: /^\s+$/.test(part) });
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return out;
|
|
184
|
+
}
|
|
185
|
+
/** Join runs that share a style, so the output has no needless span churn. */
|
|
186
|
+
function coalesce(spans) {
|
|
187
|
+
const out = [];
|
|
188
|
+
for (const span of spans) {
|
|
189
|
+
if (span.text === "")
|
|
190
|
+
continue;
|
|
191
|
+
const previous = out[out.length - 1];
|
|
192
|
+
if (previous && sameStyle(previous, span))
|
|
193
|
+
previous.text += span.text;
|
|
194
|
+
else
|
|
195
|
+
out.push({ ...span });
|
|
196
|
+
}
|
|
197
|
+
return out;
|
|
198
|
+
}
|
|
199
|
+
function sameStyle(a, b) {
|
|
200
|
+
return a.fg === b.fg && a.bg === b.bg && a.attrs === b.attrs
|
|
201
|
+
&& !!a.bold === !!b.bold && !!a.dim === !!b.dim
|
|
202
|
+
&& !!a.italic === !!b.italic && !!a.underline === !!b.underline;
|
|
203
|
+
}
|
|
204
|
+
/** Drop trailing whitespace spans, the way a soft wrap trims its line end. */
|
|
205
|
+
function trimEnd(spans) {
|
|
206
|
+
const out = spans.map((s) => ({ ...s }));
|
|
207
|
+
while (out.length > 0) {
|
|
208
|
+
const last = out[out.length - 1];
|
|
209
|
+
last.text = last.text.replace(/\s+$/, "");
|
|
210
|
+
if (last.text !== "")
|
|
211
|
+
break;
|
|
212
|
+
out.pop();
|
|
213
|
+
}
|
|
214
|
+
return out;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Greedy word wrap at `width` columns, preserving each span's style across the
|
|
218
|
+
* break. Mirrors `wrap()` in unicode.ts, including its hard split of a word
|
|
219
|
+
* longer than the line and its trimming of the soft-wrapped line end.
|
|
220
|
+
*/
|
|
221
|
+
export function wrapSpans(line, width) {
|
|
222
|
+
if (width <= 0)
|
|
223
|
+
return [];
|
|
224
|
+
const lines = [];
|
|
225
|
+
let current = [];
|
|
226
|
+
let currentWidth = 0;
|
|
227
|
+
const push = (trim) => {
|
|
228
|
+
lines.push(coalesce(trim ? trimEnd(current) : current));
|
|
229
|
+
current = [];
|
|
230
|
+
currentWidth = 0;
|
|
231
|
+
};
|
|
232
|
+
for (const token of tokenize(line)) {
|
|
233
|
+
const w = stringWidth(token.text);
|
|
234
|
+
if (currentWidth + w > width && currentWidth > 0) {
|
|
235
|
+
push(true);
|
|
236
|
+
// A break swallows the whitespace that caused it, exactly as `wrap` does.
|
|
237
|
+
if (token.space)
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
if (w > width) {
|
|
241
|
+
for (const g of graphemes(token.text)) {
|
|
242
|
+
if (currentWidth + g.width > width)
|
|
243
|
+
push(false);
|
|
244
|
+
current.push({ ...token.style, text: cellText(g.value) });
|
|
245
|
+
currentWidth += g.width;
|
|
246
|
+
}
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
current.push({ ...token.style, text: token.text });
|
|
250
|
+
currentWidth += w;
|
|
251
|
+
}
|
|
252
|
+
// The last line is trimmed like a soft-wrapped one. Only the hard split of an
|
|
253
|
+
// overlong word pushes untrimmed, because there the break is mid-word and the
|
|
254
|
+
// whitespace it would trim is real content.
|
|
255
|
+
push(true);
|
|
256
|
+
return lines;
|
|
257
|
+
}
|
|
258
|
+
/** Wrap every line of a rich value, in order. */
|
|
259
|
+
export function wrapRich(value, width) {
|
|
260
|
+
const out = [];
|
|
261
|
+
for (const line of toSpanLines(value)) {
|
|
262
|
+
if (line.length === 0) {
|
|
263
|
+
// A blank line survives wrapping as a blank line.
|
|
264
|
+
out.push([]);
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
for (const wrapped of wrapSpans(line, width))
|
|
268
|
+
out.push(wrapped);
|
|
269
|
+
}
|
|
270
|
+
return out;
|
|
271
|
+
}
|
|
272
|
+
//# sourceMappingURL=richtext.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"richtext.js","sourceRoot":"","sources":["../src/richtext.ts"],"names":[],"mappings":"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"}
|
package/dist/surface.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { FrameBuffer, type Style, type Attributes } from "./buffer.ts";
|
|
|
2
2
|
import { DEFAULT_COLOR, type Color } from "./color.ts";
|
|
3
3
|
import { type Rect, type Padding } from "./layout.ts";
|
|
4
4
|
import type { Theme } from "./theme.ts";
|
|
5
|
+
import { type SpanLine } from "./richtext.ts";
|
|
5
6
|
export type BorderStyle = "rounded" | "single" | "double" | "thick" | "dashed" | "ascii" | "none";
|
|
6
7
|
export interface BorderChars {
|
|
7
8
|
tl: string;
|
|
@@ -32,6 +33,29 @@ export declare const EDGE_LEFT = 8;
|
|
|
32
33
|
export declare function borderGlyph(style: Exclude<BorderStyle, "none">, bits: number): string | null;
|
|
33
34
|
/** The edges of a border glyph, or null when it is not one. */
|
|
34
35
|
export declare function borderBits(codepoint: number): number | null;
|
|
36
|
+
export type Side = "top" | "right" | "bottom" | "left";
|
|
37
|
+
/**
|
|
38
|
+
* Which edges of a box to draw. "all" is every side, which is what a box was
|
|
39
|
+
* before this existed; "none" draws no rule but still insets nothing, the same
|
|
40
|
+
* as a border style of "none".
|
|
41
|
+
*/
|
|
42
|
+
export type Sides = "all" | "none" | readonly Side[];
|
|
43
|
+
/** The four sides as flags, in the order the drawing code wants them. */
|
|
44
|
+
export interface DrawnSides {
|
|
45
|
+
top: boolean;
|
|
46
|
+
right: boolean;
|
|
47
|
+
bottom: boolean;
|
|
48
|
+
left: boolean;
|
|
49
|
+
}
|
|
50
|
+
export declare function resolveSides(sides: Sides | undefined): DrawnSides;
|
|
51
|
+
/**
|
|
52
|
+
* The glyph for a cell where two edges meet, given which of them are drawn.
|
|
53
|
+
*
|
|
54
|
+
* A corner is only a corner when both its sides are there. A top-only rule
|
|
55
|
+
* runs straight through the cell a corner would occupy, which is why this
|
|
56
|
+
* falls back to the plain rule rather than leaving a gap.
|
|
57
|
+
*/
|
|
58
|
+
export declare function junction(style: Exclude<BorderStyle, "none">, bits: number): string | null;
|
|
35
59
|
export type Align = "left" | "center" | "right";
|
|
36
60
|
export interface TextOptions extends Style {
|
|
37
61
|
align?: Align;
|
|
@@ -41,6 +65,11 @@ export interface TextOptions extends Style {
|
|
|
41
65
|
}
|
|
42
66
|
export interface BoxOptions extends Style {
|
|
43
67
|
border?: BorderStyle;
|
|
68
|
+
/**
|
|
69
|
+
* Which edges to draw. Defaults to all four. The interior follows the sides
|
|
70
|
+
* actually drawn, so a top-only box costs one row rather than two.
|
|
71
|
+
*/
|
|
72
|
+
sides?: Sides;
|
|
44
73
|
borderColor?: Color;
|
|
45
74
|
title?: string;
|
|
46
75
|
titleAlign?: Align;
|
|
@@ -89,6 +118,14 @@ export declare class Surface {
|
|
|
89
118
|
private mergeBorder;
|
|
90
119
|
/** Draw text at local (x, y). Returns columns written. */
|
|
91
120
|
text(x: number, y: number, text: string, options?: TextOptions): number;
|
|
121
|
+
/**
|
|
122
|
+
* Draw one line of styled spans at local (x, y). Returns columns written.
|
|
123
|
+
*
|
|
124
|
+
* The same clipping, truncation and alignment as `text`, but each span paints
|
|
125
|
+
* with its own colours over the options given here, so a line can carry more
|
|
126
|
+
* than one style. `text` is the single-span case of this.
|
|
127
|
+
*/
|
|
128
|
+
spans(x: number, y: number, line: SpanLine, options?: TextOptions): number;
|
|
92
129
|
/** Text positioned within the full surface width. */
|
|
93
130
|
textAligned(y: number, text: string, align: Align, options?: TextOptions): void;
|
|
94
131
|
fill(style: Style, ch?: number): void;
|
package/dist/surface.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"surface.d.ts","sourceRoot":"","sources":["../src/surface.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,KAAK,KAAK,EAAE,KAAK,UAAU,EAAE,MAAM,aAAa,CAAC;AACvE,OAAO,EAAE,aAAa,EAAE,KAAK,KAAK,EAAE,MAAM,YAAY,CAAC;AACvD,OAAO,EAAE,KAAK,IAAI,EAA6B,KAAK,OAAO,EAAE,MAAM,aAAa,CAAC;AAEjF,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"surface.d.ts","sourceRoot":"","sources":["../src/surface.ts"],"names":[],"mappings":"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"}
|
package/dist/surface.js
CHANGED
|
@@ -2,6 +2,7 @@ import { FrameBuffer } from "./buffer.js";
|
|
|
2
2
|
import { DEFAULT_COLOR } from "./color.js";
|
|
3
3
|
import { inset, intersect, isEmpty } from "./layout.js";
|
|
4
4
|
import { graphemes, stringWidth, truncate, fit } from "./unicode.js";
|
|
5
|
+
import { fitSpans, spanStyle, spanWidth, truncateSpans } from "./richtext.js";
|
|
5
6
|
export const BORDERS = {
|
|
6
7
|
rounded: { tl: "╭", tr: "╮", bl: "╰", br: "╯", h: "─", v: "│", ml: "├", mr: "┤", mt: "┬", mb: "┴", cross: "┼" },
|
|
7
8
|
single: { tl: "┌", tr: "┐", bl: "└", br: "┘", h: "─", v: "│", ml: "├", mr: "┤", mt: "┬", mb: "┴", cross: "┼" },
|
|
@@ -61,6 +62,35 @@ export function borderGlyph(style, bits) {
|
|
|
61
62
|
export function borderBits(codepoint) {
|
|
62
63
|
return BITS_BY_CODEPOINT.get(codepoint) ?? null;
|
|
63
64
|
}
|
|
65
|
+
export function resolveSides(sides) {
|
|
66
|
+
if (sides === undefined || sides === "all") {
|
|
67
|
+
return { top: true, right: true, bottom: true, left: true };
|
|
68
|
+
}
|
|
69
|
+
if (sides === "none")
|
|
70
|
+
return { top: false, right: false, bottom: false, left: false };
|
|
71
|
+
return {
|
|
72
|
+
top: sides.includes("top"),
|
|
73
|
+
right: sides.includes("right"),
|
|
74
|
+
bottom: sides.includes("bottom"),
|
|
75
|
+
left: sides.includes("left"),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The glyph for a cell where two edges meet, given which of them are drawn.
|
|
80
|
+
*
|
|
81
|
+
* A corner is only a corner when both its sides are there. A top-only rule
|
|
82
|
+
* runs straight through the cell a corner would occupy, which is why this
|
|
83
|
+
* falls back to the plain rule rather than leaving a gap.
|
|
84
|
+
*/
|
|
85
|
+
export function junction(style, bits) {
|
|
86
|
+
if (bits === 0)
|
|
87
|
+
return null;
|
|
88
|
+
const glyph = borderGlyph(style, bits);
|
|
89
|
+
if (glyph !== null)
|
|
90
|
+
return glyph;
|
|
91
|
+
const chars = BORDERS[style];
|
|
92
|
+
return bits & (EDGE_LEFT | EDGE_RIGHT) ? chars.h : chars.v;
|
|
93
|
+
}
|
|
64
94
|
/**
|
|
65
95
|
* A clipped, translated view onto the framebuffer. Widgets only ever see a
|
|
66
96
|
* Surface, so nothing can draw outside the rectangle it was given.
|
|
@@ -164,6 +194,45 @@ export class Surface {
|
|
|
164
194
|
}
|
|
165
195
|
return written;
|
|
166
196
|
}
|
|
197
|
+
/**
|
|
198
|
+
* Draw one line of styled spans at local (x, y). Returns columns written.
|
|
199
|
+
*
|
|
200
|
+
* The same clipping, truncation and alignment as `text`, but each span paints
|
|
201
|
+
* with its own colours over the options given here, so a line can carry more
|
|
202
|
+
* than one style. `text` is the single-span case of this.
|
|
203
|
+
*/
|
|
204
|
+
spans(x, y, line, options = {}) {
|
|
205
|
+
const ay = this.rect.y + y;
|
|
206
|
+
const c = this.clip;
|
|
207
|
+
if (ay < c.y || ay >= c.y + c.height)
|
|
208
|
+
return 0;
|
|
209
|
+
const limit = Math.min(options.maxWidth ?? this.width - x, this.width - x);
|
|
210
|
+
if (limit <= 0)
|
|
211
|
+
return 0;
|
|
212
|
+
let content = line;
|
|
213
|
+
if (options.ellipsis !== false && spanWidth(content) > limit) {
|
|
214
|
+
content = truncateSpans(content, limit);
|
|
215
|
+
}
|
|
216
|
+
if (options.align && options.align !== "left") {
|
|
217
|
+
content = fitSpans(content, limit, options.align);
|
|
218
|
+
}
|
|
219
|
+
const base = { fg: options.fg, bg: options.bg, attrs: options.attrs };
|
|
220
|
+
let cx = this.rect.x + x;
|
|
221
|
+
let written = 0;
|
|
222
|
+
for (const span of content) {
|
|
223
|
+
const style = spanStyle(span, base);
|
|
224
|
+
for (const g of graphemes(span.text)) {
|
|
225
|
+
if (written + g.width > limit)
|
|
226
|
+
return written;
|
|
227
|
+
if (cx >= c.x && cx + g.width <= c.x + c.width) {
|
|
228
|
+
this.buffer.setCell(cx, ay, g.value, style);
|
|
229
|
+
}
|
|
230
|
+
cx += g.width;
|
|
231
|
+
written += g.width;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return written;
|
|
235
|
+
}
|
|
167
236
|
/** Text positioned within the full surface width. */
|
|
168
237
|
textAligned(y, text, align, options = {}) {
|
|
169
238
|
this.text(0, y, fit(truncate(text, this.width), this.width, align), { ...options, align: "left" });
|
|
@@ -199,11 +268,29 @@ export class Surface {
|
|
|
199
268
|
const style = options.border ?? "rounded";
|
|
200
269
|
const fg = options.borderColor ?? this.theme.border;
|
|
201
270
|
const bg = options.bg;
|
|
271
|
+
const sides = resolveSides(options.sides);
|
|
272
|
+
const any = sides.top || sides.right || sides.bottom || sides.left;
|
|
273
|
+
const drawsBorder = style !== "none" && any && this.width >= 2 && this.height >= 1;
|
|
202
274
|
if (options.fill !== false && bg !== undefined) {
|
|
203
|
-
|
|
275
|
+
if (drawsBorder && options.collapse) {
|
|
276
|
+
// The border ring is left to the border drawing below, which merges
|
|
277
|
+
// with whatever a neighbour already put in the shared column. Filling
|
|
278
|
+
// it here first would erase that border, and the merge would then find
|
|
279
|
+
// a blank cell and simply overwrite it -- which is why a panel with a
|
|
280
|
+
// background collapsed its seams into a corner rather than a junction.
|
|
281
|
+
// Every cell skipped here is painted by the border, in the same bg.
|
|
282
|
+
const top = sides.top ? 1 : 0;
|
|
283
|
+
const bottom = this.height > 1 && sides.bottom ? 1 : 0;
|
|
284
|
+
const left = sides.left ? 1 : 0;
|
|
285
|
+
const right = sides.right ? 1 : 0;
|
|
286
|
+
this.fillRect(left, top, Math.max(0, this.width - left - right), Math.max(0, this.height - top - bottom), { bg });
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
this.fill({ bg });
|
|
290
|
+
}
|
|
204
291
|
}
|
|
205
|
-
if (
|
|
206
|
-
return this.inset(style === "none" ? 0 : 1);
|
|
292
|
+
if (!drawsBorder) {
|
|
293
|
+
return this.inset(style === "none" || !any ? 0 : 1);
|
|
207
294
|
}
|
|
208
295
|
const b = BORDERS[style];
|
|
209
296
|
const w = this.width;
|
|
@@ -223,22 +310,37 @@ export class Surface {
|
|
|
223
310
|
for (let i = 0; i < length; i++)
|
|
224
311
|
put(x, y + i, ch);
|
|
225
312
|
};
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
313
|
+
// A corner belongs to the two sides that meet there, so it exists only if
|
|
314
|
+
// both of them are drawn; where one is, the rule runs straight through.
|
|
315
|
+
const corner = (a, aBit, c, cBit) => junction(style, (a ? aBit : 0) | (c ? cBit : 0));
|
|
316
|
+
const tl = corner(sides.top, EDGE_RIGHT, sides.left, EDGE_DOWN);
|
|
317
|
+
const tr = corner(sides.top, EDGE_LEFT, sides.right, EDGE_DOWN);
|
|
318
|
+
if (sides.top)
|
|
319
|
+
putH(1, 0, w - 2, b.h);
|
|
320
|
+
if (tl !== null)
|
|
321
|
+
put(0, 0, tl);
|
|
322
|
+
if (tr !== null)
|
|
323
|
+
put(w - 1, 0, tr);
|
|
229
324
|
if (h > 1) {
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
325
|
+
const bl = corner(sides.bottom, EDGE_RIGHT, sides.left, EDGE_UP);
|
|
326
|
+
const br = corner(sides.bottom, EDGE_LEFT, sides.right, EDGE_UP);
|
|
327
|
+
if (sides.bottom)
|
|
328
|
+
putH(1, h - 1, w - 2, b.h);
|
|
329
|
+
if (bl !== null)
|
|
330
|
+
put(0, h - 1, bl);
|
|
331
|
+
if (br !== null)
|
|
332
|
+
put(w - 1, h - 1, br);
|
|
333
|
+
if (sides.left)
|
|
334
|
+
putV(0, 1, h - 2, b.v);
|
|
335
|
+
if (sides.right)
|
|
336
|
+
putV(w - 1, 1, h - 2, b.v);
|
|
235
337
|
}
|
|
236
338
|
// Measured before the title is drawn: both share the top border row, and
|
|
237
339
|
// the title used to be truncated against the full width and then painted
|
|
238
340
|
// over by the subtitle.
|
|
239
341
|
const subtitle = options.subtitle ? ` ${options.subtitle} ` : "";
|
|
240
342
|
const subtitleWidth = subtitle && stringWidth(subtitle) + 4 < w ? stringWidth(subtitle) : 0;
|
|
241
|
-
if (options.title) {
|
|
343
|
+
if (options.title && sides.top) {
|
|
242
344
|
const titleColor = options.titleColor ?? this.theme.title;
|
|
243
345
|
const label = ` ${options.title} `;
|
|
244
346
|
// The title lives in [2, limit). Reserving the width is not enough on its
|
|
@@ -259,20 +361,26 @@ export class Surface {
|
|
|
259
361
|
: Math.max(2, Math.min(limit - tw, Math.floor((w - tw) / 2)));
|
|
260
362
|
this.text(tx, 0, shown, { fg: titleColor, bg, attrs: 1 /* bold */ });
|
|
261
363
|
}
|
|
262
|
-
if (subtitleWidth > 0) {
|
|
364
|
+
if (subtitleWidth > 0 && sides.top) {
|
|
263
365
|
this.text(w - 2 - subtitleWidth, 0, subtitle, {
|
|
264
366
|
fg: options.subtitleColor ?? this.theme.muted,
|
|
265
367
|
bg,
|
|
266
368
|
});
|
|
267
369
|
}
|
|
268
|
-
if (options.footer && h > 2) {
|
|
370
|
+
if (options.footer && sides.bottom && h > 2) {
|
|
269
371
|
const foot = ` ${options.footer} `;
|
|
270
372
|
const fw = stringWidth(foot);
|
|
271
373
|
if (fw + 4 < w) {
|
|
272
374
|
this.text(2, h - 1, foot, { fg: options.footerColor ?? this.theme.muted, bg });
|
|
273
375
|
}
|
|
274
376
|
}
|
|
275
|
-
|
|
377
|
+
// The interior follows the sides actually drawn, so a top-only box costs
|
|
378
|
+
// one row rather than two.
|
|
379
|
+
const left = sides.left ? 1 : 0;
|
|
380
|
+
const top = sides.top ? 1 : 0;
|
|
381
|
+
const shrinkX = left + (sides.right ? 1 : 0);
|
|
382
|
+
const shrinkY = top + (sides.bottom ? 1 : 0);
|
|
383
|
+
return this.sub(left, top, Math.max(0, w - shrinkX), Math.max(0, h - shrinkY));
|
|
276
384
|
}
|
|
277
385
|
/** Absolute rect of this surface, for hit-testing mouse events. */
|
|
278
386
|
hitRect() {
|