@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.
Files changed (104) hide show
  1. package/dist/ansi.d.ts +10 -0
  2. package/dist/ansi.d.ts.map +1 -1
  3. package/dist/ansi.js +18 -0
  4. package/dist/ansi.js.map +1 -1
  5. package/dist/app.d.ts +30 -0
  6. package/dist/app.d.ts.map +1 -1
  7. package/dist/app.js +95 -12
  8. package/dist/app.js.map +1 -1
  9. package/dist/cli.js +1 -1
  10. package/dist/diff.d.ts +37 -1
  11. package/dist/diff.d.ts.map +1 -1
  12. package/dist/diff.js +79 -4
  13. package/dist/diff.js.map +1 -1
  14. package/dist/graphics/braille.d.ts.map +1 -1
  15. package/dist/graphics/braille.js +15 -2
  16. package/dist/graphics/braille.js.map +1 -1
  17. package/dist/graphics/canvas.d.ts +76 -0
  18. package/dist/graphics/canvas.d.ts.map +1 -0
  19. package/dist/graphics/canvas.js +110 -0
  20. package/dist/graphics/canvas.js.map +1 -0
  21. package/dist/graphics/chart.d.ts +70 -0
  22. package/dist/graphics/chart.d.ts.map +1 -0
  23. package/dist/graphics/chart.js +249 -0
  24. package/dist/graphics/chart.js.map +1 -0
  25. package/dist/graphics/index.d.ts +3 -0
  26. package/dist/graphics/index.d.ts.map +1 -1
  27. package/dist/graphics/index.js +3 -0
  28. package/dist/graphics/index.js.map +1 -1
  29. package/dist/graphics/world-data.d.ts +22 -0
  30. package/dist/graphics/world-data.d.ts.map +1 -0
  31. package/dist/graphics/world-data.js +1300 -0
  32. package/dist/graphics/world-data.js.map +1 -0
  33. package/dist/graphics/world.d.ts +70 -0
  34. package/dist/graphics/world.d.ts.map +1 -0
  35. package/dist/graphics/world.js +137 -0
  36. package/dist/graphics/world.js.map +1 -0
  37. package/dist/index.d.ts +5 -4
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +5 -4
  40. package/dist/index.js.map +1 -1
  41. package/dist/layout.d.ts +21 -1
  42. package/dist/layout.d.ts.map +1 -1
  43. package/dist/layout.js +54 -4
  44. package/dist/layout.js.map +1 -1
  45. package/dist/richtext.d.ts +79 -0
  46. package/dist/richtext.d.ts.map +1 -0
  47. package/dist/richtext.js +272 -0
  48. package/dist/richtext.js.map +1 -0
  49. package/dist/surface.d.ts +37 -0
  50. package/dist/surface.d.ts.map +1 -1
  51. package/dist/surface.js +123 -15
  52. package/dist/surface.js.map +1 -1
  53. package/dist/terminal.d.ts +81 -0
  54. package/dist/terminal.d.ts.map +1 -1
  55. package/dist/terminal.js +131 -4
  56. package/dist/terminal.js.map +1 -1
  57. package/dist/ui.d.ts +108 -5
  58. package/dist/ui.d.ts.map +1 -1
  59. package/dist/ui.js +134 -11
  60. package/dist/ui.js.map +1 -1
  61. package/dist/unicode.d.ts +9 -0
  62. package/dist/unicode.d.ts.map +1 -1
  63. package/dist/unicode.js +25 -0
  64. package/dist/unicode.js.map +1 -1
  65. package/dist/widgets/calendar.d.ts +67 -0
  66. package/dist/widgets/calendar.d.ts.map +1 -0
  67. package/dist/widgets/calendar.js +118 -0
  68. package/dist/widgets/calendar.js.map +1 -0
  69. package/dist/widgets/chart.d.ts +24 -0
  70. package/dist/widgets/chart.d.ts.map +1 -0
  71. package/dist/widgets/chart.js +80 -0
  72. package/dist/widgets/chart.js.map +1 -0
  73. package/dist/widgets/index.d.ts +6 -0
  74. package/dist/widgets/index.d.ts.map +1 -1
  75. package/dist/widgets/index.js +6 -0
  76. package/dist/widgets/index.js.map +1 -1
  77. package/dist/widgets/meters.d.ts.map +1 -1
  78. package/dist/widgets/meters.js +11 -1
  79. package/dist/widgets/meters.js.map +1 -1
  80. package/dist/widgets/scrollbar.d.ts +66 -0
  81. package/dist/widgets/scrollbar.d.ts.map +1 -0
  82. package/dist/widgets/scrollbar.js +89 -0
  83. package/dist/widgets/scrollbar.js.map +1 -0
  84. package/dist/widgets/shadow.d.ts +42 -0
  85. package/dist/widgets/shadow.d.ts.map +1 -0
  86. package/dist/widgets/shadow.js +73 -0
  87. package/dist/widgets/shadow.js.map +1 -0
  88. package/dist/widgets/surface.d.ts +33 -0
  89. package/dist/widgets/surface.d.ts.map +1 -0
  90. package/dist/widgets/surface.js +37 -0
  91. package/dist/widgets/surface.js.map +1 -0
  92. package/dist/widgets/table.d.ts +1 -2
  93. package/dist/widgets/table.d.ts.map +1 -1
  94. package/dist/widgets/table.js +4 -12
  95. package/dist/widgets/table.js.map +1 -1
  96. package/dist/widgets/text.d.ts +12 -1
  97. package/dist/widgets/text.d.ts.map +1 -1
  98. package/dist/widgets/text.js +27 -3
  99. package/dist/widgets/text.js.map +1 -1
  100. package/dist/widgets/world.d.ts +32 -0
  101. package/dist/widgets/world.d.ts.map +1 -0
  102. package/dist/widgets/world.js +31 -0
  103. package/dist/widgets/world.js.map +1 -0
  104. 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"}
@@ -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;
@@ -1 +1 @@
1
- {"version":3,"file":"surface.d.ts","sourceRoot":"","sources":["../src/surface.ts"],"names":[],"mappings":"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"}
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
- this.fill({ bg });
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 (style === "none" || this.width < 2 || this.height < 1) {
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
- put(0, 0, b.tl);
227
- put(w - 1, 0, b.tr);
228
- putH(1, 0, w - 2, b.h);
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
- put(0, h - 1, b.bl);
231
- put(w - 1, h - 1, b.br);
232
- putH(1, h - 1, w - 2, b.h);
233
- putV(0, 1, h - 2, b.v);
234
- putV(w - 1, 1, h - 2, b.v);
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
- return this.sub(1, 1, Math.max(0, w - 2), Math.max(0, h - 2));
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() {