@lewishowles/cli-style 0.17.0 → 0.17.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/CHANGELOG.md +18 -1
- package/adapters/python/pyproject.toml +1 -1
- package/package.json +4 -1
- package/src/formatters/ansi.js +187 -2
- package/src/formatters/wrap.js +24 -18
- package/src/gallery/render-gallery.js +79 -13
- package/src/primitives/bar-chart.js +3 -3
- package/src/primitives/chip.js +10 -2
- package/src/primitives/divider.js +2 -2
- package/src/primitives/feedback.js +8 -2
- package/src/primitives/labelled-line.js +20 -10
- package/src/primitives/panel.js +20 -5
- package/src/primitives/row-group.js +8 -6
- package/src/primitives/row.js +5 -5
- package/src/primitives/span.js +12 -3
- package/src/primitives/status.js +8 -2
- package/src/primitives/table.js +30 -16
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## 0.17.1 - 2026-10-20
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- The package now depends on `get-east-asian-width` to measure wide characters.
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Spans, chips, status lines and empty states now keep their own colour, weight and chip background after caller-coloured text.
|
|
12
|
+
- Labelled lines now keep their bold prefix and tone colour after a caller-coloured icon or label.
|
|
13
|
+
- Panels now keep their background, text colour and bold title to the end of a line that holds coloured text.
|
|
14
|
+
- Panels now fill every line to the panel width when it holds coloured text or wide characters.
|
|
15
|
+
- Text wrapping now measures coloured text, wide characters, and joined emoji by the columns they fill. It keeps joined emoji and accented letters whole, and wraps before a wide character would pass the limit.
|
|
16
|
+
- `stripAnsi()` now removes terminal hyperlinks and other terminal control sequences as well as colours.
|
|
17
|
+
- Tables now line up columns when values contain coloured or wide text.
|
|
18
|
+
- Tables now cut overflowing cells by visible columns while keeping colours and terminal links.
|
|
19
|
+
- Rows, row groups and labelled lines now line up when labels contain coloured or wide text.
|
|
20
|
+
- Dividers and bar charts now line up when labels contain coloured or wide text.
|
|
4
21
|
|
|
5
22
|
## 0.17.0 - 2026-10-03
|
|
6
23
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lewishowles/cli-style",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.1",
|
|
4
4
|
"description": "Shared terminal output styles for Lewis Howles tools, diagnostics, and package CLIs.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -38,6 +38,9 @@
|
|
|
38
38
|
"smoke:binary": "./dist/bin/cli-style render status --plain < tests/fixtures/render/status-success.json",
|
|
39
39
|
"test:unit": "bun test"
|
|
40
40
|
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"get-east-asian-width": "^1.7.0"
|
|
43
|
+
},
|
|
41
44
|
"devDependencies": {
|
|
42
45
|
"@stylistic/eslint-plugin": "^5.10.0",
|
|
43
46
|
"vite": "npm:@voidzero-dev/vite-plus-core@0.2.8",
|
package/src/formatters/ansi.js
CHANGED
|
@@ -1,11 +1,42 @@
|
|
|
1
|
+
import { eastAsianWidth } from "get-east-asian-width";
|
|
2
|
+
|
|
1
3
|
import { resolveColourValue } from "../theme/colours.js";
|
|
2
4
|
|
|
3
5
|
// ANSI reset sequence used after coloured output.
|
|
4
6
|
const reset = "\u001b[0m";
|
|
5
7
|
|
|
6
|
-
//
|
|
8
|
+
// The escape character that starts every ANSI sequence.
|
|
7
9
|
const escapeCharacter = String.fromCharCode(27);
|
|
8
|
-
|
|
10
|
+
|
|
11
|
+
// The bell character that can end an OSC sequence, such as a terminal hyperlink.
|
|
12
|
+
const bellCharacter = String.fromCharCode(7);
|
|
13
|
+
|
|
14
|
+
// Matches ANSI CSI sequences, such as colours, and OSC sequences, such as hyperlinks, which take up no columns on screen.
|
|
15
|
+
export const ansiPattern = new RegExp(
|
|
16
|
+
`${escapeCharacter}(?:\\[[0-?]*[ -/]*[@-~]|\\][^${bellCharacter}${escapeCharacter}]*(?:${bellCharacter}|${escapeCharacter}\\\\))`,
|
|
17
|
+
"gu",
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
// Matches the same sequences as a captured group, so splitting text on it keeps each sequence as its own piece.
|
|
21
|
+
const ansiSplitPattern = new RegExp(`(${ansiPattern.source})`, "u");
|
|
22
|
+
|
|
23
|
+
// Matches a terminal link sequence and captures its address, which is empty when the sequence ends a link.
|
|
24
|
+
const hyperlinkPattern = new RegExp(
|
|
25
|
+
`^${escapeCharacter}\\]8;[^;]*;([^${bellCharacter}${escapeCharacter}]*)`,
|
|
26
|
+
"u",
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
// The sequences that clear every colour and text style at once.
|
|
30
|
+
const clearStyleCodes = [reset, "\u001b[m"];
|
|
31
|
+
|
|
32
|
+
// Splits text into the characters people see, so a joined emoji or an accented letter counts as one.
|
|
33
|
+
const graphemeSegmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
|
|
34
|
+
|
|
35
|
+
// Matches emoji presentation characters or the selector that requests emoji presentation.
|
|
36
|
+
const emojiPattern = /[\p{Emoji_Presentation}\uFE0F]/u;
|
|
37
|
+
|
|
38
|
+
// Matches a cluster made only of combining marks or invisible characters, such as a lone joiner, which take no columns.
|
|
39
|
+
const zeroWidthPattern = /^[\p{Mark}\p{Default_Ignorable_Code_Point}]+$/u;
|
|
9
40
|
|
|
10
41
|
// ANSI style sequences supported before primitive renderers exist.
|
|
11
42
|
const styleCodes = {
|
|
@@ -73,6 +104,36 @@ export function style(value, style, options = {}) {
|
|
|
73
104
|
return `${styleCode}${value}${reset}`;
|
|
74
105
|
}
|
|
75
106
|
|
|
107
|
+
/**
|
|
108
|
+
* Switch the given styles back on after every full reset inside text, so an outer style
|
|
109
|
+
* carries on past coloured text placed inside it. Coloured text ends with a full reset,
|
|
110
|
+
* written as `\u001b[0m` or the shorter `\u001b[m`, which would otherwise turn the outer
|
|
111
|
+
* style off as well.
|
|
112
|
+
*
|
|
113
|
+
* @param {string} value
|
|
114
|
+
* Text that may hold coloured parts.
|
|
115
|
+
* @param {string[]} stylesToRestore
|
|
116
|
+
* The styles to switch back on, each rendered on an empty string, such as
|
|
117
|
+
* `style("", "bold", options)`. An empty entry adds nothing.
|
|
118
|
+
* @returns {string}
|
|
119
|
+
* The text with the styles switched back on after each full reset.
|
|
120
|
+
*/
|
|
121
|
+
export function restoreStyles(value, stylesToRestore) {
|
|
122
|
+
// The codes that turn the styles on, with each closing reset cut off.
|
|
123
|
+
const openingCodes = stylesToRestore
|
|
124
|
+
.map((styleToRestore) => styleToRestore.slice(0, -reset.length))
|
|
125
|
+
.join("");
|
|
126
|
+
|
|
127
|
+
// The text with the styles switched back on after each reset form handled so far.
|
|
128
|
+
let restoredValue = value;
|
|
129
|
+
|
|
130
|
+
for (const clearStyleCode of clearStyleCodes) {
|
|
131
|
+
restoredValue = restoredValue.replaceAll(clearStyleCode, `${clearStyleCode}${openingCodes}`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return restoredValue;
|
|
135
|
+
}
|
|
136
|
+
|
|
76
137
|
/**
|
|
77
138
|
* Remove ANSI escape sequences from text.
|
|
78
139
|
*
|
|
@@ -85,6 +146,130 @@ export function stripAnsi(value) {
|
|
|
85
146
|
return value.replace(ansiPattern, "");
|
|
86
147
|
}
|
|
87
148
|
|
|
149
|
+
/**
|
|
150
|
+
* Count the terminal columns that text fills on screen. ANSI sequences take no
|
|
151
|
+
* columns, and East Asian wide characters and emoji take two.
|
|
152
|
+
*
|
|
153
|
+
* @param {string} value
|
|
154
|
+
* The text to measure, which may contain ANSI sequences.
|
|
155
|
+
* @returns {number}
|
|
156
|
+
* The number of columns the text fills.
|
|
157
|
+
*/
|
|
158
|
+
export function visibleWidth(value) {
|
|
159
|
+
let width = 0;
|
|
160
|
+
|
|
161
|
+
for (const { segment } of graphemeSegmenter.segment(stripAnsi(value))) {
|
|
162
|
+
if (zeroWidthPattern.test(segment)) {
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (eastAsianWidth(segment.codePointAt(0)) === 2 || emojiPattern.test(segment)) {
|
|
167
|
+
width += 2;
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
width += 1;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return width;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Add trailing spaces until text fills a number of terminal columns. Colour
|
|
179
|
+
* codes take no space and wide characters take two, so coloured and wide text
|
|
180
|
+
* pads to the same width as plain text.
|
|
181
|
+
*
|
|
182
|
+
* @param {string} value
|
|
183
|
+
* The text to pad, which may contain ANSI colour codes.
|
|
184
|
+
* @param {number} width
|
|
185
|
+
* The number of columns the text should fill.
|
|
186
|
+
* @returns {string}
|
|
187
|
+
* The padded text, or the text unchanged when it already fills the width.
|
|
188
|
+
*/
|
|
189
|
+
export function padVisible(value, width) {
|
|
190
|
+
return value + " ".repeat(Math.max(0, width - visibleWidth(value)));
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Split text at a terminal column without breaking a colour code, a link or a
|
|
195
|
+
* character. A character that would cross the column, such as a two-column 界
|
|
196
|
+
* at the edge, starts the remaining text instead.
|
|
197
|
+
*
|
|
198
|
+
* When the cut falls inside coloured or linked text, the first piece ends by
|
|
199
|
+
* closing the colour and the link, and the remaining text starts by reopening
|
|
200
|
+
* them, so each piece can be printed on a line of its own.
|
|
201
|
+
*
|
|
202
|
+
* @param {string} value
|
|
203
|
+
* The text to split, which may contain ANSI colour codes and terminal links.
|
|
204
|
+
* @param {number} width
|
|
205
|
+
* The most columns the first piece may fill.
|
|
206
|
+
* @returns {{ head: string, tail: string }}
|
|
207
|
+
* The first piece and the remaining text, which is empty when the whole
|
|
208
|
+
* value fits.
|
|
209
|
+
*/
|
|
210
|
+
export function sliceVisible(value, width) {
|
|
211
|
+
// Splitting on a captured pattern puts plain text at even indexes and ANSI sequences at odd ones.
|
|
212
|
+
const parts = value.split(ansiSplitPattern);
|
|
213
|
+
|
|
214
|
+
// The link open at the current position, with the sequences that close and reopen it.
|
|
215
|
+
let activeLink = null;
|
|
216
|
+
// The colour and style codes in effect at the current position, in the order they were set.
|
|
217
|
+
let activeStyles = [];
|
|
218
|
+
// The columns filled so far.
|
|
219
|
+
let columns = 0;
|
|
220
|
+
// The position in the original value reached so far.
|
|
221
|
+
let cursor = 0;
|
|
222
|
+
|
|
223
|
+
for (const [index, part] of parts.entries()) {
|
|
224
|
+
if (index % 2 === 0) {
|
|
225
|
+
for (const { segment } of graphemeSegmenter.segment(part)) {
|
|
226
|
+
const segmentWidth = visibleWidth(segment);
|
|
227
|
+
|
|
228
|
+
if (columns + segmentWidth > width) {
|
|
229
|
+
const head =
|
|
230
|
+
value.slice(0, cursor) +
|
|
231
|
+
(activeLink?.closer ?? "") +
|
|
232
|
+
(activeStyles.length > 0 ? reset : "");
|
|
233
|
+
|
|
234
|
+
const tail = activeStyles.join("") + (activeLink?.opener ?? "") + value.slice(cursor);
|
|
235
|
+
|
|
236
|
+
return { head, tail };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
columns += segmentWidth;
|
|
240
|
+
cursor += segment.length;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (clearStyleCodes.includes(part)) {
|
|
247
|
+
activeStyles = [];
|
|
248
|
+
} else if (part.startsWith(`${escapeCharacter}[`) && part.endsWith("m")) {
|
|
249
|
+
activeStyles.push(part);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const hyperlink = part.match(hyperlinkPattern);
|
|
253
|
+
|
|
254
|
+
if (hyperlink !== null) {
|
|
255
|
+
// The closing sequence ends the same way as the opening one, with a bell or an escape and backslash.
|
|
256
|
+
const terminator = part.endsWith(bellCharacter) ? bellCharacter : `${escapeCharacter}\\`;
|
|
257
|
+
|
|
258
|
+
activeLink =
|
|
259
|
+
hyperlink[1] === ""
|
|
260
|
+
? null
|
|
261
|
+
: {
|
|
262
|
+
closer: `${escapeCharacter}]8;;${terminator}`,
|
|
263
|
+
opener: part,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
cursor += part.length;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return { head: value, tail: "" };
|
|
271
|
+
}
|
|
272
|
+
|
|
88
273
|
/**
|
|
89
274
|
* Wrap text in ANSI colour codes.
|
|
90
275
|
*
|
package/src/formatters/wrap.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
+
import { ansiPattern, visibleWidth } from "./ansi.js";
|
|
2
|
+
|
|
1
3
|
// Width used when a caller doesn't provide one.
|
|
2
4
|
const defaultWrapWidth = 64;
|
|
3
|
-
const escapeCharacter = String.fromCharCode(27);
|
|
4
|
-
const bellCharacter = String.fromCharCode(7);
|
|
5
5
|
|
|
6
|
-
//
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
// Splits text into the characters people see, so wrapping never breaks a joined emoji apart.
|
|
7
|
+
const graphemeSegmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
|
|
8
|
+
|
|
9
|
+
// Line breaks end the current wrapped line and take no columns. Windows CR+LF arrives from the
|
|
10
|
+
// segmenter as one character.
|
|
11
|
+
const lineBreaks = new Set(["\n", "\r\n"]);
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Wrap text to a maximum visible width, treating ANSI escape sequences as zero-width.
|
|
@@ -27,7 +28,7 @@ export function wrapText(text, width = defaultWrapWidth) {
|
|
|
27
28
|
let line = [];
|
|
28
29
|
|
|
29
30
|
for (const token of tokens) {
|
|
30
|
-
if (token.value
|
|
31
|
+
if (lineBreaks.has(token.value)) {
|
|
31
32
|
lines.push(renderTokens(line));
|
|
32
33
|
line = [];
|
|
33
34
|
continue;
|
|
@@ -35,7 +36,7 @@ export function wrapText(text, width = defaultWrapWidth) {
|
|
|
35
36
|
|
|
36
37
|
line.push(token);
|
|
37
38
|
|
|
38
|
-
while (
|
|
39
|
+
while (tokensWidth(line) > wrapWidth) {
|
|
39
40
|
const whitespaceIndex = findLastWhitespace(line);
|
|
40
41
|
|
|
41
42
|
if (whitespaceIndex > 0) {
|
|
@@ -77,7 +78,7 @@ function resolveWrapWidth(width) {
|
|
|
77
78
|
}
|
|
78
79
|
|
|
79
80
|
/**
|
|
80
|
-
* Split text into
|
|
81
|
+
* Split text into the characters people see and ANSI escape sequences.
|
|
81
82
|
*
|
|
82
83
|
* @param {string} text
|
|
83
84
|
* Text to tokenise.
|
|
@@ -89,7 +90,7 @@ function tokenise(text) {
|
|
|
89
90
|
|
|
90
91
|
let textStart = 0;
|
|
91
92
|
|
|
92
|
-
for (const match of text.matchAll(
|
|
93
|
+
for (const match of text.matchAll(ansiPattern)) {
|
|
93
94
|
appendTextTokens(tokens, text.slice(textStart, match.index));
|
|
94
95
|
tokens.push({
|
|
95
96
|
value: match[0],
|
|
@@ -104,7 +105,9 @@ function tokenise(text) {
|
|
|
104
105
|
}
|
|
105
106
|
|
|
106
107
|
/**
|
|
107
|
-
* Append one token per character of plain text to a token list
|
|
108
|
+
* Append one token per visible character of plain text to a token list, keeping
|
|
109
|
+
* joined emoji and accented letters whole. Line breaks, including Windows CR+LF,
|
|
110
|
+
* take no columns.
|
|
108
111
|
*
|
|
109
112
|
* @param {object[]} tokens
|
|
110
113
|
* Token list to append to.
|
|
@@ -113,10 +116,10 @@ function tokenise(text) {
|
|
|
113
116
|
* @returns {void}
|
|
114
117
|
*/
|
|
115
118
|
function appendTextTokens(tokens, text) {
|
|
116
|
-
for (const value of text) {
|
|
119
|
+
for (const { segment: value } of graphemeSegmenter.segment(text)) {
|
|
117
120
|
tokens.push({
|
|
118
121
|
value,
|
|
119
|
-
visibleWidth: value
|
|
122
|
+
visibleWidth: lineBreaks.has(value) ? 0 : visibleWidth(value),
|
|
120
123
|
});
|
|
121
124
|
}
|
|
122
125
|
}
|
|
@@ -129,7 +132,7 @@ function appendTextTokens(tokens, text) {
|
|
|
129
132
|
* @returns {number}
|
|
130
133
|
* Total visible width.
|
|
131
134
|
*/
|
|
132
|
-
function
|
|
135
|
+
function tokensWidth(tokens) {
|
|
133
136
|
return tokens.reduce((width, token) => width + token.visibleWidth, 0);
|
|
134
137
|
}
|
|
135
138
|
|
|
@@ -155,6 +158,7 @@ function findLastWhitespace(tokens) {
|
|
|
155
158
|
|
|
156
159
|
/**
|
|
157
160
|
* Find the index to split at when a line has no whitespace to break on.
|
|
161
|
+
* A single character wider than the limit stays whole on its own line.
|
|
158
162
|
*
|
|
159
163
|
* @param {object[]} tokens
|
|
160
164
|
* Tokens to search.
|
|
@@ -167,11 +171,13 @@ function findHardSplitIndex(tokens, width) {
|
|
|
167
171
|
let currentWidth = 0;
|
|
168
172
|
|
|
169
173
|
for (let index = 0; index < tokens.length; index += 1) {
|
|
170
|
-
currentWidth
|
|
174
|
+
const nextWidth = currentWidth + tokens[index].visibleWidth;
|
|
171
175
|
|
|
172
|
-
if (currentWidth
|
|
173
|
-
return index
|
|
176
|
+
if (nextWidth > width && currentWidth > 0) {
|
|
177
|
+
return index;
|
|
174
178
|
}
|
|
179
|
+
|
|
180
|
+
currentWidth = nextWidth;
|
|
175
181
|
}
|
|
176
182
|
|
|
177
183
|
return tokens.length;
|
|
@@ -8,7 +8,7 @@ import { diffBlock } from "../patterns/diff-block.js";
|
|
|
8
8
|
import { nextStepBlock } from "../patterns/next-step-block.js";
|
|
9
9
|
import { quoteBlock } from "../patterns/quote-block.js";
|
|
10
10
|
import { taskSummary } from "../patterns/task-summary.js";
|
|
11
|
-
import {
|
|
11
|
+
import { foreground, sliceVisible, visibleWidth } from "../formatters/ansi.js";
|
|
12
12
|
import { renderSpinnerFrame } from "../interactive/spinner.js";
|
|
13
13
|
import { barChart } from "../primitives/bar-chart.js";
|
|
14
14
|
import { bulletList } from "../primitives/bullet-list.js";
|
|
@@ -303,27 +303,26 @@ function clampGalleryOutput(output, width) {
|
|
|
303
303
|
}
|
|
304
304
|
|
|
305
305
|
/**
|
|
306
|
-
*
|
|
306
|
+
* Cut one gallery line to the gallery width, measured in screen columns.
|
|
307
307
|
*
|
|
308
308
|
* @param {string} line
|
|
309
|
-
*
|
|
309
|
+
* The output line, which may contain colour codes and wide characters.
|
|
310
310
|
* @param {number} width
|
|
311
|
-
*
|
|
311
|
+
* The most columns the line may fill.
|
|
312
312
|
* @returns {string}
|
|
313
|
-
*
|
|
313
|
+
* The line unchanged when it fits, or cut with a trailing `~` that marks
|
|
314
|
+
* the cut. Colour codes and links stay whole and are closed before the `~`.
|
|
314
315
|
*/
|
|
315
316
|
function clampLine(line, width) {
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
if (plainLine.length <= width) {
|
|
317
|
+
if (visibleWidth(line) <= width) {
|
|
319
318
|
return line;
|
|
320
319
|
}
|
|
321
320
|
|
|
322
321
|
if (width <= 1) {
|
|
323
|
-
return
|
|
322
|
+
return sliceVisible(line, width).head;
|
|
324
323
|
}
|
|
325
324
|
|
|
326
|
-
return `${
|
|
325
|
+
return `${sliceVisible(line, width - 1).head}~`;
|
|
327
326
|
}
|
|
328
327
|
|
|
329
328
|
/**
|
|
@@ -760,9 +759,21 @@ function renderPrimitives(options) {
|
|
|
760
759
|
"",
|
|
761
760
|
"Chips",
|
|
762
761
|
renderChipExamples(options),
|
|
762
|
+
chip(`${foreground("coloured", "danger", options)} label`, "success", options),
|
|
763
763
|
"",
|
|
764
764
|
"Statuses",
|
|
765
765
|
...renderStatusExamples(options),
|
|
766
|
+
status(resultTypes.SUCCESS, "caller-coloured label", {
|
|
767
|
+
...options,
|
|
768
|
+
label: `${foreground("Coloured", "danger", options)} status`,
|
|
769
|
+
}),
|
|
770
|
+
"",
|
|
771
|
+
"Dividers",
|
|
772
|
+
divider({
|
|
773
|
+
...options,
|
|
774
|
+
dividerWidth: 32,
|
|
775
|
+
label: foreground("界面", "info", options),
|
|
776
|
+
}),
|
|
766
777
|
"",
|
|
767
778
|
"Panels",
|
|
768
779
|
...renderPanelExamples(options),
|
|
@@ -804,6 +815,15 @@ function renderPrimitives(options) {
|
|
|
804
815
|
icon: "!",
|
|
805
816
|
tone: "warning",
|
|
806
817
|
}),
|
|
818
|
+
labelledLine(
|
|
819
|
+
foreground("界面", "info", options),
|
|
820
|
+
"Coloured wide labels keep wrapped text aligned.",
|
|
821
|
+
{
|
|
822
|
+
...options,
|
|
823
|
+
icon: foreground("界", "warning", options),
|
|
824
|
+
width: 32,
|
|
825
|
+
},
|
|
826
|
+
),
|
|
807
827
|
emptyState("No results", "tone: muted", options),
|
|
808
828
|
errorBlock("Failed", ["tone: danger"], {
|
|
809
829
|
...options,
|
|
@@ -836,6 +856,16 @@ function renderPrimitives(options) {
|
|
|
836
856
|
...options,
|
|
837
857
|
labelWidth: 8,
|
|
838
858
|
}),
|
|
859
|
+
row(
|
|
860
|
+
foreground("界面", "info", options),
|
|
861
|
+
"Coloured wide labels and separators align wrapped text.",
|
|
862
|
+
{
|
|
863
|
+
...options,
|
|
864
|
+
labelWidth: 8,
|
|
865
|
+
separator: foreground(" => ", "warning", options),
|
|
866
|
+
width: 36,
|
|
867
|
+
},
|
|
868
|
+
),
|
|
839
869
|
row("Bundle", "is 22.3 KB, above the 12.0 KB budget", {
|
|
840
870
|
...options,
|
|
841
871
|
labelWidth: 8,
|
|
@@ -879,6 +909,10 @@ function renderPrimitives(options) {
|
|
|
879
909
|
label: "Checks",
|
|
880
910
|
value: "type-check, unit, build",
|
|
881
911
|
},
|
|
912
|
+
{
|
|
913
|
+
label: foreground("界面", "info", options),
|
|
914
|
+
value: "Coloured wide labels share the value column",
|
|
915
|
+
},
|
|
882
916
|
{
|
|
883
917
|
label: "Bundle",
|
|
884
918
|
result: resultTypes.FAILED,
|
|
@@ -904,6 +938,10 @@ function renderPrimitives(options) {
|
|
|
904
938
|
function renderBulletListExamples(options) {
|
|
905
939
|
return [
|
|
906
940
|
bulletList(["Install dependencies", "Run the unit tests", "Review the output"], options),
|
|
941
|
+
bulletList([foreground("界面 item wraps beside its bullet", "info", options)], {
|
|
942
|
+
...options,
|
|
943
|
+
width: 18,
|
|
944
|
+
}),
|
|
907
945
|
bulletList(["This item wraps so continuation lines stay aligned with the item text."], {
|
|
908
946
|
...options,
|
|
909
947
|
width: 40,
|
|
@@ -959,6 +997,10 @@ function renderTableExample(options) {
|
|
|
959
997
|
check: "unit",
|
|
960
998
|
result: "warning",
|
|
961
999
|
},
|
|
1000
|
+
{
|
|
1001
|
+
check: "界面",
|
|
1002
|
+
result: foreground("passed", "success", options),
|
|
1003
|
+
},
|
|
962
1004
|
],
|
|
963
1005
|
});
|
|
964
1006
|
}
|
|
@@ -990,7 +1032,7 @@ function renderOverflowTableExample(options, overflow) {
|
|
|
990
1032
|
],
|
|
991
1033
|
rows: [
|
|
992
1034
|
{
|
|
993
|
-
detail: "packages/cli-style/src/primitives/table.js",
|
|
1035
|
+
detail: foreground("界面/packages/cli-style/src/primitives/table.js", "success", options),
|
|
994
1036
|
source: "Table",
|
|
995
1037
|
},
|
|
996
1038
|
],
|
|
@@ -1009,6 +1051,10 @@ function renderBarChartExample(options) {
|
|
|
1009
1051
|
return barChart({
|
|
1010
1052
|
...options,
|
|
1011
1053
|
rows: [
|
|
1054
|
+
{
|
|
1055
|
+
label: foreground("界面", "info", options),
|
|
1056
|
+
value: 8,
|
|
1057
|
+
},
|
|
1012
1058
|
{
|
|
1013
1059
|
label: "positive",
|
|
1014
1060
|
value: 10,
|
|
@@ -1115,7 +1161,7 @@ function renderProgressExamples(options) {
|
|
|
1115
1161
|
}
|
|
1116
1162
|
|
|
1117
1163
|
/**
|
|
1118
|
-
* Render
|
|
1164
|
+
* Render one panel for each tone, then a panel holding coloured output.
|
|
1119
1165
|
*
|
|
1120
1166
|
* @param {object} options
|
|
1121
1167
|
* Rendering options.
|
|
@@ -1123,7 +1169,8 @@ function renderProgressExamples(options) {
|
|
|
1123
1169
|
* Panel examples.
|
|
1124
1170
|
*/
|
|
1125
1171
|
function renderPanelExamples(options) {
|
|
1126
|
-
|
|
1172
|
+
// One panel for each tone.
|
|
1173
|
+
const toneExamples = panelToneExamples.map((tone) =>
|
|
1127
1174
|
panel({
|
|
1128
1175
|
...options,
|
|
1129
1176
|
lines: [`tone: ${tone}`],
|
|
@@ -1131,6 +1178,25 @@ function renderPanelExamples(options) {
|
|
|
1131
1178
|
tone,
|
|
1132
1179
|
}),
|
|
1133
1180
|
);
|
|
1181
|
+
|
|
1182
|
+
// A panel holding a coloured status line and coloured values, to show the panel
|
|
1183
|
+
// background carrying on after them.
|
|
1184
|
+
const nestedStylesExample = panel({
|
|
1185
|
+
...options,
|
|
1186
|
+
lines: [
|
|
1187
|
+
status(resultTypes.SUCCESS, "Checks passed", options),
|
|
1188
|
+
rowGroup({
|
|
1189
|
+
...options,
|
|
1190
|
+
rows: [
|
|
1191
|
+
{ label: "Source", value: "main", valueColour: "success" },
|
|
1192
|
+
{ label: "Checks", value: "unit", valueColour: "success" },
|
|
1193
|
+
],
|
|
1194
|
+
}),
|
|
1195
|
+
],
|
|
1196
|
+
title: "Nested styles",
|
|
1197
|
+
});
|
|
1198
|
+
|
|
1199
|
+
return [...toneExamples, nestedStylesExample];
|
|
1134
1200
|
}
|
|
1135
1201
|
|
|
1136
1202
|
/**
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { foreground } from "../formatters/ansi.js";
|
|
1
|
+
import { foreground, padVisible, visibleWidth } from "../formatters/ansi.js";
|
|
2
2
|
import { chartColours } from "../theme/colours.js";
|
|
3
3
|
|
|
4
4
|
// Chart bars use the same compact default width as progress bars.
|
|
@@ -27,7 +27,7 @@ export function barChart(options = {}) {
|
|
|
27
27
|
return "";
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
const labelWidth = Math.max(...rows.map((row) => row.label
|
|
30
|
+
const labelWidth = Math.max(...rows.map((row) => visibleWidth(row.label)));
|
|
31
31
|
const maximum = Math.max(...rows.map((row) => Math.abs(row.value)), 1);
|
|
32
32
|
const width = normaliseWidth(options.barWidth);
|
|
33
33
|
|
|
@@ -93,7 +93,7 @@ function renderRow(row, labelWidth, maximum, width, options) {
|
|
|
93
93
|
const barWidth = Math.round((Math.abs(row.value) / maximum) * width);
|
|
94
94
|
const symbol = options.unicode === false ? "#" : "█";
|
|
95
95
|
const bar = symbol.repeat(barWidth).padEnd(width, " ");
|
|
96
|
-
const label = row.label
|
|
96
|
+
const label = padVisible(row.label, labelWidth);
|
|
97
97
|
|
|
98
98
|
if (options.colour !== true) {
|
|
99
99
|
return `${label} ${bar} ${row.value}`;
|
package/src/primitives/chip.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { background, foreground, style } from "../formatters/ansi.js";
|
|
1
|
+
import { background, foreground, restoreStyles, style } from "../formatters/ansi.js";
|
|
2
2
|
import { chipColours } from "../theme/colours.js";
|
|
3
3
|
|
|
4
4
|
// Chip style used when the requested tone is not configured.
|
|
@@ -25,7 +25,15 @@ export function chip(label, tone = fallbackTone, options = {}) {
|
|
|
25
25
|
return `[${label}]`;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
// The label with the chip's fill, bold and tone switched back on after any coloured text,
|
|
29
|
+
// so the rest of the chip keeps its look.
|
|
30
|
+
const restoredLabel = restoreStyles(label, [
|
|
31
|
+
background("", token.background, options),
|
|
32
|
+
style("", "bold", options),
|
|
33
|
+
foreground("", token.foreground, options),
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
const paddedLabel = ` ${restoredLabel} `;
|
|
29
37
|
const colouredLabel = foreground(paddedLabel, token.foreground, options);
|
|
30
38
|
const boldLabel = style(colouredLabel, "bold", options);
|
|
31
39
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { foreground } from "../formatters/ansi.js";
|
|
1
|
+
import { foreground, visibleWidth } from "../formatters/ansi.js";
|
|
2
2
|
|
|
3
3
|
// Dividers use enough width to read as a section break in compact output.
|
|
4
4
|
const defaultWidth = 40;
|
|
@@ -33,7 +33,7 @@ export function divider(options = {}) {
|
|
|
33
33
|
const character = options.character ?? defaultCharacter;
|
|
34
34
|
const label = options.label ?? "";
|
|
35
35
|
const width = options.dividerWidth ?? defaultWidth;
|
|
36
|
-
const lineWidth = Math.max(width - label
|
|
36
|
+
const lineWidth = Math.max(width - visibleWidth(label) - labelGap(label), 0);
|
|
37
37
|
const line = character.repeat(lineWidth);
|
|
38
38
|
const output = label === "" ? line : `${label} ${line}`;
|
|
39
39
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { foreground, style } from "../formatters/ansi.js";
|
|
1
|
+
import { foreground, restoreStyles, style } from "../formatters/ansi.js";
|
|
2
2
|
import { getResultSymbol, resultTypes } from "../theme/results.js";
|
|
3
3
|
import { labelledLine } from "./labelled-line.js";
|
|
4
4
|
import { panel } from "./panel.js";
|
|
@@ -48,7 +48,13 @@ export function emptyState(title = "No results", detail = "", options = {}) {
|
|
|
48
48
|
return `${prefix}${detailText}`;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
// The prefix with its bold and muted tone switched back on after a coloured title.
|
|
52
|
+
const restoredPrefix = restoreStyles(prefix, [
|
|
53
|
+
style("", "bold", options),
|
|
54
|
+
foreground("", "muted", options),
|
|
55
|
+
]);
|
|
56
|
+
|
|
57
|
+
const renderedPrefix = style(foreground(restoredPrefix, "muted", options), "bold", options);
|
|
52
58
|
|
|
53
59
|
return `${renderedPrefix}${detailText}`;
|
|
54
60
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { foreground, style } from "../formatters/ansi.js";
|
|
1
|
+
import { foreground, restoreStyles, style, visibleWidth } from "../formatters/ansi.js";
|
|
2
2
|
import { wrapText } from "../formatters/wrap.js";
|
|
3
3
|
|
|
4
4
|
// Falls back to the informational tone when no colour token is given.
|
|
@@ -44,20 +44,19 @@ export function labelledLine(label = "", message = "", options = {}) {
|
|
|
44
44
|
return `${prefix}${messageSeparator}${messageText}`;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
const wrapWidth = resolveWrapWidth(
|
|
47
|
+
const wrapWidth = resolveWrapWidth(prefix, options);
|
|
48
48
|
const messageLines = wrapText(messageText, wrapWidth);
|
|
49
|
-
const continuationIndent = " ".repeat(
|
|
49
|
+
const continuationIndent = " ".repeat(visibleWidth(prefix) + visibleWidth(messageSeparator));
|
|
50
50
|
const wrappedMessage = messageLines.join(`\n${continuationIndent}`);
|
|
51
51
|
|
|
52
52
|
return `${prefix}${messageSeparator}${wrappedMessage}`;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
/**
|
|
56
|
-
* Colour the icon
|
|
57
|
-
* continuation indent lines up whether or not colour is applied.
|
|
56
|
+
* Colour the icon and label prefix in the line's tone when colour is on.
|
|
58
57
|
*
|
|
59
58
|
* @param {string} prefix
|
|
60
|
-
*
|
|
59
|
+
* Icon and label prefix. The icon or label may already hold colour.
|
|
61
60
|
* @param {object} options
|
|
62
61
|
* Rendering options.
|
|
63
62
|
* @returns {string}
|
|
@@ -72,13 +71,24 @@ function renderPrefix(prefix, options) {
|
|
|
72
71
|
return prefix;
|
|
73
72
|
}
|
|
74
73
|
|
|
75
|
-
|
|
74
|
+
// The caller's tone, or "info" when none is given.
|
|
75
|
+
const tone = resolveColour(options);
|
|
76
|
+
|
|
77
|
+
// The prefix with bold and the line's colour switched back on after a coloured icon or
|
|
78
|
+
// label, so the rest of the prefix keeps them.
|
|
79
|
+
const restoredPrefix = restoreStyles(prefix, [
|
|
80
|
+
style("", "bold", options),
|
|
81
|
+
foreground("", tone, options),
|
|
82
|
+
]);
|
|
83
|
+
|
|
84
|
+
// The whole prefix in the line's colour, ready to be made bold.
|
|
85
|
+
const colouredPrefix = foreground(restoredPrefix, tone, options);
|
|
76
86
|
|
|
77
87
|
return style(colouredPrefix, "bold", options);
|
|
78
88
|
}
|
|
79
89
|
|
|
80
90
|
/**
|
|
81
|
-
* Build the
|
|
91
|
+
* Build the icon and label prefix. The icon or label may already hold colour.
|
|
82
92
|
*
|
|
83
93
|
* @param {string} label
|
|
84
94
|
* Label text.
|
|
@@ -115,7 +125,7 @@ function resolveColour(options) {
|
|
|
115
125
|
* Work out how wide the message can be before it wraps.
|
|
116
126
|
*
|
|
117
127
|
* @param {string} prefix
|
|
118
|
-
*
|
|
128
|
+
* The icon and label prefix as it will be printed, including any colour.
|
|
119
129
|
* @param {object} options
|
|
120
130
|
* Rendering options.
|
|
121
131
|
* @returns {number|undefined}
|
|
@@ -127,7 +137,7 @@ function resolveWrapWidth(prefix, options) {
|
|
|
127
137
|
}
|
|
128
138
|
|
|
129
139
|
if (Number.isFinite(options.width)) {
|
|
130
|
-
return Math.max(1, options.width - prefix
|
|
140
|
+
return Math.max(1, options.width - visibleWidth(prefix) - visibleWidth(messageSeparator));
|
|
131
141
|
}
|
|
132
142
|
|
|
133
143
|
return undefined;
|
package/src/primitives/panel.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { background, foreground, style } from "../formatters/ansi.js";
|
|
1
|
+
import { background, foreground, restoreStyles, style, visibleWidth } from "../formatters/ansi.js";
|
|
2
2
|
import { wrapText } from "../formatters/wrap.js";
|
|
3
3
|
import { profiles } from "../profiles/profiles.js";
|
|
4
4
|
import { panelColours } from "../theme/colours.js";
|
|
@@ -64,23 +64,38 @@ export function panel(options = {}) {
|
|
|
64
64
|
* @param {object} options
|
|
65
65
|
* Rendering options.
|
|
66
66
|
* @returns {string[]}
|
|
67
|
-
* The panel lines, each exactly `width` columns wide
|
|
67
|
+
* The panel lines, each exactly `width` columns wide, unless a wide character
|
|
68
|
+
* does not fit in a panel at its minimum width.
|
|
68
69
|
*/
|
|
69
70
|
function renderLine(line, width, isTitle, options) {
|
|
70
71
|
const accent = options.unicode === false ? "|" : "▌";
|
|
71
72
|
const contentWidth = width - 5;
|
|
73
|
+
// Whether this line is drawn with ANSI colour.
|
|
74
|
+
const useColour = options.colour === true;
|
|
72
75
|
|
|
73
76
|
return wrapText(line, contentWidth).map((visibleLine) => {
|
|
74
|
-
|
|
77
|
+
// Spaces that fill the line to the content width on screen. A wide character can
|
|
78
|
+
// overflow a very narrow panel, so the count never drops below zero.
|
|
79
|
+
const padding = " ".repeat(Math.max(0, contentWidth - visibleWidth(visibleLine)));
|
|
80
|
+
// The line with its inner margins, ready for the panel colours.
|
|
81
|
+
const content = ` ${visibleLine}${padding} `;
|
|
75
82
|
|
|
76
|
-
if (
|
|
83
|
+
if (!useColour) {
|
|
77
84
|
return `${accent}${content}`;
|
|
78
85
|
}
|
|
79
86
|
|
|
80
87
|
const renderedAccent = renderPanelColour(accent, options);
|
|
81
88
|
|
|
89
|
+
// The content with the panel colours switched back on after each reset inside it, so
|
|
90
|
+
// the panel background carries on to the right edge.
|
|
91
|
+
const restoredContent = restoreStyles(content, [
|
|
92
|
+
background("", panelColours.background, options),
|
|
93
|
+
foreground("", panelColours.body, options),
|
|
94
|
+
isTitle ? style("", "bold", options) : "",
|
|
95
|
+
]);
|
|
96
|
+
|
|
82
97
|
const renderedContent = foreground(
|
|
83
|
-
background(
|
|
98
|
+
background(restoredContent, panelColours.background, options),
|
|
84
99
|
panelColours.body,
|
|
85
100
|
options,
|
|
86
101
|
);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { visibleWidth } from "../formatters/ansi.js";
|
|
1
2
|
import { getResultSymbol } from "../theme/results.js";
|
|
2
3
|
import { row } from "./row.js";
|
|
3
4
|
|
|
@@ -21,7 +22,7 @@ export function rowGroup(options = {}) {
|
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
const labelWidth = Math.max(
|
|
24
|
-
...rows.map((entry) =>
|
|
25
|
+
...rows.map((entry) => displayedLabelWidth(entry, options)),
|
|
25
26
|
options.labelWidth ?? 0,
|
|
26
27
|
);
|
|
27
28
|
|
|
@@ -59,18 +60,19 @@ function normaliseRows(rows) {
|
|
|
59
60
|
}
|
|
60
61
|
|
|
61
62
|
/**
|
|
62
|
-
*
|
|
63
|
+
* Count the columns a row's label fills, including its result symbol when it
|
|
64
|
+
* has one.
|
|
63
65
|
*
|
|
64
66
|
* @param {object} entry
|
|
65
67
|
* Row entry.
|
|
66
68
|
* @param {object} options
|
|
67
69
|
* Group rendering options.
|
|
68
70
|
* @returns {number}
|
|
69
|
-
*
|
|
71
|
+
* The number of terminal columns the label takes up.
|
|
70
72
|
*/
|
|
71
|
-
function
|
|
73
|
+
function displayedLabelWidth(entry, options) {
|
|
72
74
|
if (typeof entry.result !== "string" || entry.result === "") {
|
|
73
|
-
return entry.label
|
|
75
|
+
return visibleWidth(entry.label);
|
|
74
76
|
}
|
|
75
77
|
|
|
76
78
|
const entryOptions = {
|
|
@@ -80,5 +82,5 @@ function displayedLabelLength(entry, options) {
|
|
|
80
82
|
|
|
81
83
|
const symbol = getResultSymbol(entry.result, entryOptions);
|
|
82
84
|
|
|
83
|
-
return [symbol, entry.label].join(" ")
|
|
85
|
+
return visibleWidth([symbol, entry.label].join(" "));
|
|
84
86
|
}
|
package/src/primitives/row.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { foreground } from "../formatters/ansi.js";
|
|
1
|
+
import { foreground, padVisible, visibleWidth } from "../formatters/ansi.js";
|
|
2
2
|
import { wrapText } from "../formatters/wrap.js";
|
|
3
3
|
import { getResultSymbol, getResultToken } from "../theme/results.js";
|
|
4
4
|
|
|
@@ -49,8 +49,8 @@ const minimumWrapWidth = 10;
|
|
|
49
49
|
export function row(label, value = "", options = {}) {
|
|
50
50
|
const resultToken = resolveResultToken(options);
|
|
51
51
|
const displayLabel = renderDisplayLabel(label, resultToken, options);
|
|
52
|
-
const labelWidth = Math.max(displayLabel
|
|
53
|
-
const labelText = displayLabel
|
|
52
|
+
const labelWidth = Math.max(visibleWidth(displayLabel), options.labelWidth ?? 0);
|
|
53
|
+
const labelText = padVisible(displayLabel, labelWidth);
|
|
54
54
|
const renderedLabel = renderLabel(labelText, options);
|
|
55
55
|
|
|
56
56
|
if (value === "") {
|
|
@@ -66,7 +66,7 @@ export function row(label, value = "", options = {}) {
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
const availableWrapWidth = Number.isFinite(options.width)
|
|
69
|
-
? options.width - labelWidth - separator
|
|
69
|
+
? options.width - labelWidth - visibleWidth(separator)
|
|
70
70
|
: defaultWrapWidth;
|
|
71
71
|
|
|
72
72
|
const hasExplicitWrapWidth = options.wrapWidth !== undefined && options.wrapWidth !== null;
|
|
@@ -78,7 +78,7 @@ export function row(label, value = "", options = {}) {
|
|
|
78
78
|
const wrapWidth = options.wrapWidth ?? availableWrapWidth;
|
|
79
79
|
|
|
80
80
|
const valueLines = wrapText(value, wrapWidth);
|
|
81
|
-
const continuationIndent = " ".repeat(labelWidth + separator
|
|
81
|
+
const continuationIndent = " ".repeat(labelWidth + visibleWidth(separator));
|
|
82
82
|
const renderedValueLines = valueLines.map((line) => renderValue(line, options));
|
|
83
83
|
const wrappedValue = renderedValueLines.join(`\n${continuationIndent}`);
|
|
84
84
|
|
package/src/primitives/span.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { foreground, style } from "../formatters/ansi.js";
|
|
1
|
+
import { foreground, restoreStyles, style } from "../formatters/ansi.js";
|
|
2
2
|
|
|
3
3
|
// Inline spans use the same informational tone as hints by default.
|
|
4
4
|
const defaultTone = "info";
|
|
@@ -27,7 +27,16 @@ export function span(value = "", tone = defaultTone, options = {}) {
|
|
|
27
27
|
return value;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
// The text weight, bold unless the options ask for another.
|
|
31
|
+
const weight = options.weight ?? defaultWeight;
|
|
31
32
|
|
|
32
|
-
|
|
33
|
+
// The text with the span's weight and tone switched back on after any coloured text.
|
|
34
|
+
const restoredValue = restoreStyles(value, [
|
|
35
|
+
style("", weight, options),
|
|
36
|
+
foreground("", tone, options),
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
const renderedValue = foreground(restoredValue, tone, options);
|
|
40
|
+
|
|
41
|
+
return style(renderedValue, weight, options);
|
|
33
42
|
}
|
package/src/primitives/status.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { foreground, style } from "../formatters/ansi.js";
|
|
1
|
+
import { foreground, restoreStyles, style } from "../formatters/ansi.js";
|
|
2
2
|
import { getResultSymbol, getResultToken, resultTypes } from "../theme/results.js";
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -33,7 +33,13 @@ export function status(resultType = resultTypes.UNKNOWN, detail = "", options =
|
|
|
33
33
|
return `${prefix}${detailText}`;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
// The prefix with its bold and tone switched back on after a coloured label.
|
|
37
|
+
const restoredPrefix = restoreStyles(prefix, [
|
|
38
|
+
style("", "bold", options),
|
|
39
|
+
foreground("", token.tone, options),
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
const colouredPrefix = foreground(restoredPrefix, token.tone, options);
|
|
37
43
|
const boldPrefix = style(colouredPrefix, "bold", options);
|
|
38
44
|
|
|
39
45
|
return `${boldPrefix}${detailText}`;
|
package/src/primitives/table.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { foreground } from "../formatters/ansi.js";
|
|
1
|
+
import { foreground, padVisible, sliceVisible, visibleWidth } from "../formatters/ansi.js";
|
|
2
2
|
import { defaultWidth, minimumWidth, normaliseWidth } from "../formatters/width.js";
|
|
3
3
|
import { tableColours } from "../theme/colours.js";
|
|
4
4
|
|
|
@@ -112,8 +112,8 @@ function normaliseRows(rows) {
|
|
|
112
112
|
function columnWidths(columns, rows) {
|
|
113
113
|
return columns.map((column) => {
|
|
114
114
|
const naturalWidth = Math.max(
|
|
115
|
-
column.label
|
|
116
|
-
...rows.map((row) => cellValue(row[column.key])
|
|
115
|
+
visibleWidth(column.label),
|
|
116
|
+
...rows.map((row) => visibleWidth(cellValue(row[column.key]))),
|
|
117
117
|
);
|
|
118
118
|
|
|
119
119
|
const maxWidth = columnMaxWidth(column);
|
|
@@ -151,24 +151,38 @@ function columnMaxWidth(column) {
|
|
|
151
151
|
* @param {number} width
|
|
152
152
|
* Column width.
|
|
153
153
|
* @returns {string[]}
|
|
154
|
-
* Lines the cell renders as
|
|
155
|
-
*
|
|
154
|
+
* Lines the cell renders as, each no wider on screen than the column
|
|
155
|
+
* unless one character is wider than the whole column. A value that does
|
|
156
|
+
* not fit is cut to one line with a trailing ellipsis, or split across
|
|
157
|
+
* lines when the column's overflow is 'wrap'. Coloured text keeps its
|
|
158
|
+
* colour on every line.
|
|
156
159
|
*/
|
|
157
160
|
function columnLines(value, column, width) {
|
|
158
|
-
if (value
|
|
161
|
+
if (visibleWidth(value) <= width) {
|
|
159
162
|
return [value];
|
|
160
163
|
}
|
|
161
164
|
|
|
162
165
|
if (column.overflow === "wrap") {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
166
|
+
const lines = [];
|
|
167
|
+
|
|
168
|
+
let remaining = value;
|
|
169
|
+
|
|
170
|
+
while (remaining !== "") {
|
|
171
|
+
let { head, tail } = sliceVisible(remaining, width);
|
|
172
|
+
|
|
173
|
+
if (visibleWidth(head) === 0 && tail !== "") {
|
|
174
|
+
// In a one-column cell, a two-column character gets a line of its own so wrapping always moves forward.
|
|
175
|
+
({ head, tail } = sliceVisible(remaining, 2));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
lines.push(head);
|
|
179
|
+
remaining = tail;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return lines;
|
|
169
183
|
}
|
|
170
184
|
|
|
171
|
-
return [`${value
|
|
185
|
+
return [`${sliceVisible(value, width - 1).head}…`];
|
|
172
186
|
}
|
|
173
187
|
|
|
174
188
|
/**
|
|
@@ -242,7 +256,7 @@ function renderTable(columns, rows, widths, options) {
|
|
|
242
256
|
* Padded column text.
|
|
243
257
|
*/
|
|
244
258
|
function padColumn(value, width, index, columnCount) {
|
|
245
|
-
return index === columnCount - 1 ? value : value
|
|
259
|
+
return index === columnCount - 1 ? value : padVisible(value, width);
|
|
246
260
|
}
|
|
247
261
|
|
|
248
262
|
/**
|
|
@@ -258,13 +272,13 @@ function padColumn(value, width, index, columnCount) {
|
|
|
258
272
|
* Rendered key/value blocks.
|
|
259
273
|
*/
|
|
260
274
|
function renderBlocks(columns, rows, options) {
|
|
261
|
-
const labelWidth = Math.max(...columns.map((column) => column.label
|
|
275
|
+
const labelWidth = Math.max(...columns.map((column) => visibleWidth(column.label)));
|
|
262
276
|
|
|
263
277
|
return rows
|
|
264
278
|
.map((row) =>
|
|
265
279
|
columns
|
|
266
280
|
.map((column) => {
|
|
267
|
-
const label = renderHeader(column.label
|
|
281
|
+
const label = renderHeader(padVisible(column.label, labelWidth), options);
|
|
268
282
|
const value = renderCell(cellValue(row[column.key]), 0, options);
|
|
269
283
|
|
|
270
284
|
return `${label}${columnSeparator}${value}`;
|