@lewishowles/cli-style 0.11.0 → 0.12.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/CHANGELOG.md +7 -0
- package/package.json +4 -4
- package/src/catalogue/renderer-catalogue.js +9 -2
- package/src/cli/render-command.js +6 -0
- package/src/formatters/wrap.js +220 -0
- package/src/gallery/render-gallery.js +27 -0
- package/src/index.js +1 -0
- package/src/primitives/feedback.js +6 -9
- package/src/primitives/labelled-line.js +134 -0
- package/src/primitives/row.js +26 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.12.0 - 2026-08-20
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added `labelledLine(label, message, options)`, a general icon/label/message renderer with a configurable tone. `hint()` now delegates to it instead of duplicating its own rendering, with unchanged output for existing callers.
|
|
10
|
+
- `row()` now wraps long values within the available row width (64 columns by default), instead of overflowing the line. Override with `wrapWidth`, or disable with `wrap: false`.
|
|
11
|
+
|
|
5
12
|
## 0.11.0 - 2026-07-24
|
|
6
13
|
|
|
7
14
|
### Added
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lewishowles/cli-style",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "Shared terminal output styles for Lewis Howles tools, diagnostics, and package CLIs.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -32,14 +32,14 @@
|
|
|
32
32
|
"check": "vp check --fix",
|
|
33
33
|
"cli-style:gallery": "bun ./bin/cli-style.js gallery",
|
|
34
34
|
"package:binary": "scripts/package-binary.sh",
|
|
35
|
-
"prepare": "vp config",
|
|
35
|
+
"prepare": "vp config --no-agent",
|
|
36
36
|
"smoke:binary": "./dist/bin/cli-style render status --plain < tests/fixtures/render/status-success.json",
|
|
37
37
|
"test:unit": "bun test"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@stylistic/eslint-plugin": "^5.10.0",
|
|
41
|
-
"vite": "npm:@voidzero-dev/vite-plus-core@0.2.
|
|
42
|
-
"vite-plus": "0.2.
|
|
41
|
+
"vite": "npm:@voidzero-dev/vite-plus-core@0.2.8",
|
|
42
|
+
"vite-plus": "0.2.8"
|
|
43
43
|
},
|
|
44
44
|
"engines": {
|
|
45
45
|
"node": ">=20"
|
|
@@ -25,11 +25,18 @@ export const rendererCatalogue = [
|
|
|
25
25
|
["empty-state", "primitive", "emptyState", ["detail", "title"], undefined],
|
|
26
26
|
["error-block", "primitive", "errorBlock", ["lines", "title"], undefined],
|
|
27
27
|
["hint", "primitive", "hint", ["message"], undefined],
|
|
28
|
+
[
|
|
29
|
+
"labelled-line",
|
|
30
|
+
"primitive",
|
|
31
|
+
"labelledLine",
|
|
32
|
+
["icon", "label", "message", "wrap", "wrapWidth"],
|
|
33
|
+
undefined,
|
|
34
|
+
],
|
|
28
35
|
["next-step-block", "pattern", "nextStepBlock", ["next", "reason"], "next-step-block"],
|
|
29
36
|
["panel", "primitive", "panel", ["lines", "title", "tone"], undefined],
|
|
30
37
|
["progress-bar", "primitive", "progressBar", ["max", "value"], undefined],
|
|
31
|
-
["row", "primitive", "row", ["label", "value"], undefined],
|
|
32
|
-
["row-group", "primitive", "rowGroup", ["rows"], undefined],
|
|
38
|
+
["row", "primitive", "row", ["label", "value", "wrap", "wrapWidth"], undefined],
|
|
39
|
+
["row-group", "primitive", "rowGroup", ["rows", "wrap", "wrapWidth"], undefined],
|
|
33
40
|
["sparkline", "primitive", "sparkline", ["values"], "sparkline"],
|
|
34
41
|
["span", "primitive", "span", ["tone", "value"], undefined],
|
|
35
42
|
["status", "primitive", "status", ["detail", "type"], undefined],
|
|
@@ -11,6 +11,7 @@ import { barChart } from "../primitives/bar-chart.js";
|
|
|
11
11
|
import { chip } from "../primitives/chip.js";
|
|
12
12
|
import { divider } from "../primitives/divider.js";
|
|
13
13
|
import { emptyState, errorBlock, hint } from "../primitives/feedback.js";
|
|
14
|
+
import { labelledLine } from "../primitives/labelled-line.js";
|
|
14
15
|
import { panel } from "../primitives/panel.js";
|
|
15
16
|
import { progressBar } from "../primitives/progress-bar.js";
|
|
16
17
|
import { row } from "../primitives/row.js";
|
|
@@ -62,6 +63,11 @@ const renderers = {
|
|
|
62
63
|
...input,
|
|
63
64
|
...options,
|
|
64
65
|
}),
|
|
66
|
+
"labelled-line": (input, options) =>
|
|
67
|
+
labelledLine(input.label, input.message, {
|
|
68
|
+
...input,
|
|
69
|
+
...options,
|
|
70
|
+
}),
|
|
65
71
|
"next-step-block": (input, options) => nextStepBlock(input, options),
|
|
66
72
|
panel: (input, options) =>
|
|
67
73
|
panel({
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
// Width used when a caller doesn't provide one.
|
|
2
|
+
const defaultWrapWidth = 64;
|
|
3
|
+
const escapeCharacter = String.fromCharCode(27);
|
|
4
|
+
const bellCharacter = String.fromCharCode(7);
|
|
5
|
+
|
|
6
|
+
// Matches ANSI CSI and OSC escape sequences so they can be measured as zero-width.
|
|
7
|
+
const ansiEscapePattern = new RegExp(
|
|
8
|
+
`${escapeCharacter}(?:\\[[0-?]*[ -/]*[@-~]|\\][^${bellCharacter}]*(?:${bellCharacter}|${escapeCharacter}\\\\))`,
|
|
9
|
+
"gu",
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Wrap text to a maximum visible width, treating ANSI escape sequences as zero-width.
|
|
14
|
+
*
|
|
15
|
+
* @param {string} text
|
|
16
|
+
* Text to wrap.
|
|
17
|
+
* @param {number} width
|
|
18
|
+
* Maximum visible width per line.
|
|
19
|
+
* @returns {string[]}
|
|
20
|
+
* Wrapped lines.
|
|
21
|
+
*/
|
|
22
|
+
export function wrapText(text, width = defaultWrapWidth) {
|
|
23
|
+
const wrapWidth = resolveWrapWidth(width);
|
|
24
|
+
const tokens = tokenise(String(text));
|
|
25
|
+
const lines = [];
|
|
26
|
+
|
|
27
|
+
let line = [];
|
|
28
|
+
|
|
29
|
+
for (const token of tokens) {
|
|
30
|
+
if (token.value === "\n") {
|
|
31
|
+
lines.push(renderTokens(line));
|
|
32
|
+
line = [];
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
line.push(token);
|
|
37
|
+
|
|
38
|
+
while (visibleWidth(line) > wrapWidth) {
|
|
39
|
+
const whitespaceIndex = findLastWhitespace(line);
|
|
40
|
+
|
|
41
|
+
if (whitespaceIndex > 0) {
|
|
42
|
+
const left = line.slice(0, whitespaceIndex);
|
|
43
|
+
|
|
44
|
+
lines.push(renderTokens(left));
|
|
45
|
+
line = trimLeadingWhitespace(line.slice(whitespaceIndex + 1));
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const splitIndex = findHardSplitIndex(line, wrapWidth);
|
|
50
|
+
|
|
51
|
+
lines.push(renderTokens(line.slice(0, splitIndex)));
|
|
52
|
+
line = line.slice(splitIndex);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
lines.push(renderTokens(line));
|
|
57
|
+
|
|
58
|
+
return lines;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Resolve a usable wrap width, falling back to the default for invalid input.
|
|
63
|
+
*
|
|
64
|
+
* @param {number} width
|
|
65
|
+
* Requested wrap width.
|
|
66
|
+
* @returns {number}
|
|
67
|
+
* Whole-number wrap width of at least 1.
|
|
68
|
+
*/
|
|
69
|
+
function resolveWrapWidth(width) {
|
|
70
|
+
const numericWidth = Number(width);
|
|
71
|
+
|
|
72
|
+
if (!Number.isFinite(numericWidth) || numericWidth < 1) {
|
|
73
|
+
return defaultWrapWidth;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return Math.floor(numericWidth);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Split text into visible characters and ANSI escape sequences.
|
|
81
|
+
*
|
|
82
|
+
* @param {string} text
|
|
83
|
+
* Text to tokenise.
|
|
84
|
+
* @returns {object[]}
|
|
85
|
+
* Tokens, each with a `value` and its `visibleWidth`.
|
|
86
|
+
*/
|
|
87
|
+
function tokenise(text) {
|
|
88
|
+
const tokens = [];
|
|
89
|
+
|
|
90
|
+
let textStart = 0;
|
|
91
|
+
|
|
92
|
+
for (const match of text.matchAll(ansiEscapePattern)) {
|
|
93
|
+
appendTextTokens(tokens, text.slice(textStart, match.index));
|
|
94
|
+
tokens.push({
|
|
95
|
+
value: match[0],
|
|
96
|
+
visibleWidth: 0,
|
|
97
|
+
});
|
|
98
|
+
textStart = match.index + match[0].length;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
appendTextTokens(tokens, text.slice(textStart));
|
|
102
|
+
|
|
103
|
+
return tokens;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Append one token per character of plain text to a token list.
|
|
108
|
+
*
|
|
109
|
+
* @param {object[]} tokens
|
|
110
|
+
* Token list to append to.
|
|
111
|
+
* @param {string} text
|
|
112
|
+
* Plain text, with no ANSI escape sequences.
|
|
113
|
+
* @returns {void}
|
|
114
|
+
*/
|
|
115
|
+
function appendTextTokens(tokens, text) {
|
|
116
|
+
for (const value of text) {
|
|
117
|
+
tokens.push({
|
|
118
|
+
value,
|
|
119
|
+
visibleWidth: value === "\n" ? 0 : 1,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Sum the visible width of a list of tokens.
|
|
126
|
+
*
|
|
127
|
+
* @param {object[]} tokens
|
|
128
|
+
* Tokens to measure.
|
|
129
|
+
* @returns {number}
|
|
130
|
+
* Total visible width.
|
|
131
|
+
*/
|
|
132
|
+
function visibleWidth(tokens) {
|
|
133
|
+
return tokens.reduce((width, token) => width + token.visibleWidth, 0);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Find the index of the last whitespace token, to break a line at a word boundary.
|
|
138
|
+
*
|
|
139
|
+
* @param {object[]} tokens
|
|
140
|
+
* Tokens to search.
|
|
141
|
+
* @returns {number}
|
|
142
|
+
* Index of the last whitespace token, or -1 when none is found.
|
|
143
|
+
*/
|
|
144
|
+
function findLastWhitespace(tokens) {
|
|
145
|
+
for (let index = tokens.length - 1; index >= 0; index -= 1) {
|
|
146
|
+
const token = tokens[index];
|
|
147
|
+
|
|
148
|
+
if (token.visibleWidth === 1 && /\s/u.test(token.value)) {
|
|
149
|
+
return index;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return -1;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Find the index to split at when a line has no whitespace to break on.
|
|
158
|
+
*
|
|
159
|
+
* @param {object[]} tokens
|
|
160
|
+
* Tokens to search.
|
|
161
|
+
* @param {number} width
|
|
162
|
+
* Maximum visible width for the first segment.
|
|
163
|
+
* @returns {number}
|
|
164
|
+
* Index at which to split the tokens.
|
|
165
|
+
*/
|
|
166
|
+
function findHardSplitIndex(tokens, width) {
|
|
167
|
+
let currentWidth = 0;
|
|
168
|
+
|
|
169
|
+
for (let index = 0; index < tokens.length; index += 1) {
|
|
170
|
+
currentWidth += tokens[index].visibleWidth;
|
|
171
|
+
|
|
172
|
+
if (currentWidth >= width) {
|
|
173
|
+
return index + 1;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return tokens.length;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Remove leading whitespace tokens from a wrapped continuation line.
|
|
182
|
+
*
|
|
183
|
+
* @param {object[]} tokens
|
|
184
|
+
* Tokens to trim.
|
|
185
|
+
* @returns {object[]}
|
|
186
|
+
* Tokens with leading whitespace removed.
|
|
187
|
+
*/
|
|
188
|
+
function trimLeadingWhitespace(tokens) {
|
|
189
|
+
const trimmedTokens = [];
|
|
190
|
+
|
|
191
|
+
let hasVisibleText = false;
|
|
192
|
+
|
|
193
|
+
for (const token of tokens) {
|
|
194
|
+
if (token.visibleWidth === 0) {
|
|
195
|
+
trimmedTokens.push(token);
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (!hasVisibleText && /\s/u.test(token.value)) {
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
hasVisibleText = true;
|
|
204
|
+
trimmedTokens.push(token);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return trimmedTokens;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Join a list of tokens back into a string.
|
|
212
|
+
*
|
|
213
|
+
* @param {object[]} tokens
|
|
214
|
+
* Tokens to render.
|
|
215
|
+
* @returns {string}
|
|
216
|
+
* Rendered text.
|
|
217
|
+
*/
|
|
218
|
+
function renderTokens(tokens) {
|
|
219
|
+
return tokens.map((token) => token.value).join("");
|
|
220
|
+
}
|
|
@@ -13,6 +13,7 @@ import { barChart } from "../primitives/bar-chart.js";
|
|
|
13
13
|
import { chip } from "../primitives/chip.js";
|
|
14
14
|
import { divider } from "../primitives/divider.js";
|
|
15
15
|
import { emptyState, errorBlock, hint } from "../primitives/feedback.js";
|
|
16
|
+
import { labelledLine } from "../primitives/labelled-line.js";
|
|
16
17
|
import { panel } from "../primitives/panel.js";
|
|
17
18
|
import { progressBar } from "../primitives/progress-bar.js";
|
|
18
19
|
import { row } from "../primitives/row.js";
|
|
@@ -771,6 +772,11 @@ function renderPrimitives(options) {
|
|
|
771
772
|
"",
|
|
772
773
|
"Feedback",
|
|
773
774
|
hint(`Run ${span("npm run docs:readme", "info", options)}`, options),
|
|
775
|
+
labelledLine("Notice", "Reusable labelled output", {
|
|
776
|
+
...options,
|
|
777
|
+
icon: "!",
|
|
778
|
+
tone: "warning",
|
|
779
|
+
}),
|
|
774
780
|
emptyState("No results", "tone: muted", options),
|
|
775
781
|
errorBlock("Failed", ["tone: danger"], {
|
|
776
782
|
...options,
|
|
@@ -808,6 +814,23 @@ function renderPrimitives(options) {
|
|
|
808
814
|
labelWidth: 8,
|
|
809
815
|
result: resultTypes.FAILED,
|
|
810
816
|
}),
|
|
817
|
+
row(
|
|
818
|
+
"Description",
|
|
819
|
+
"This value wraps at the default width so long diagnostic details remain readable in a terminal.",
|
|
820
|
+
{
|
|
821
|
+
...options,
|
|
822
|
+
labelWidth: 11,
|
|
823
|
+
},
|
|
824
|
+
),
|
|
825
|
+
row(
|
|
826
|
+
"Raw output",
|
|
827
|
+
"This long value keeps its original line because wrapping is disabled for raw output.",
|
|
828
|
+
{
|
|
829
|
+
...options,
|
|
830
|
+
labelWidth: 11,
|
|
831
|
+
wrap: false,
|
|
832
|
+
},
|
|
833
|
+
),
|
|
811
834
|
"",
|
|
812
835
|
"Row groups",
|
|
813
836
|
rowGroup({
|
|
@@ -830,6 +853,10 @@ function renderPrimitives(options) {
|
|
|
830
853
|
result: resultTypes.FAILED,
|
|
831
854
|
value: "is 22.3 KB, above the 12.0 KB budget",
|
|
832
855
|
},
|
|
856
|
+
{
|
|
857
|
+
label: "Description",
|
|
858
|
+
value: "This group value inherits the default wrapping used by each row in the group.",
|
|
859
|
+
},
|
|
833
860
|
],
|
|
834
861
|
}),
|
|
835
862
|
].join("\n");
|
package/src/index.js
CHANGED
|
@@ -43,6 +43,7 @@ export { barChart } from "./primitives/bar-chart.js";
|
|
|
43
43
|
export { chip } from "./primitives/chip.js";
|
|
44
44
|
export { divider } from "./primitives/divider.js";
|
|
45
45
|
export { emptyState, errorBlock, hint } from "./primitives/feedback.js";
|
|
46
|
+
export { labelledLine } from "./primitives/labelled-line.js";
|
|
46
47
|
export { panel } from "./primitives/panel.js";
|
|
47
48
|
export { progressBar } from "./primitives/progress-bar.js";
|
|
48
49
|
export { row } from "./primitives/row.js";
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { foreground, style } from "../formatters/ansi.js";
|
|
2
2
|
import { getResultSymbol, resultTypes } from "../theme/results.js";
|
|
3
|
+
import { labelledLine } from "./labelled-line.js";
|
|
3
4
|
import { panel } from "./panel.js";
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -15,15 +16,11 @@ import { panel } from "./panel.js";
|
|
|
15
16
|
* Rendered hint.
|
|
16
17
|
*/
|
|
17
18
|
export function hint(message = "", options = {}) {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const renderedPrefix = style(foreground(prefix, "info", options), "bold", options);
|
|
25
|
-
|
|
26
|
-
return `${renderedPrefix} ${message}`;
|
|
19
|
+
return labelledLine("Hint", message, {
|
|
20
|
+
icon: "i",
|
|
21
|
+
tone: "info",
|
|
22
|
+
...options,
|
|
23
|
+
});
|
|
27
24
|
}
|
|
28
25
|
|
|
29
26
|
/**
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { foreground, style } from "../formatters/ansi.js";
|
|
2
|
+
import { wrapText } from "../formatters/wrap.js";
|
|
3
|
+
|
|
4
|
+
// Falls back to the informational tone when no colour token is given.
|
|
5
|
+
const defaultColour = "info";
|
|
6
|
+
|
|
7
|
+
// Sits between the colon-terminated prefix and the message.
|
|
8
|
+
const messageSeparator = " ";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Render an icon, label, and message as one wrapped line.
|
|
12
|
+
*
|
|
13
|
+
* @param {string} label
|
|
14
|
+
* Label shown before the message; omit for an unlabelled line.
|
|
15
|
+
* @param {string} message
|
|
16
|
+
* Message text.
|
|
17
|
+
* @param {object} options
|
|
18
|
+
* Rendering options.
|
|
19
|
+
* @param {boolean} options.colour
|
|
20
|
+
* Whether ANSI colour should be applied.
|
|
21
|
+
* @param {string} options.icon
|
|
22
|
+
* Optional icon shown before the label.
|
|
23
|
+
* @param {string} options.tone
|
|
24
|
+
* Colour token for the icon and label when colour is enabled; defaults to "info".
|
|
25
|
+
* @param {number} options.width
|
|
26
|
+
* Total line width; the message width is this minus the label and separator.
|
|
27
|
+
* @param {boolean} options.wrap
|
|
28
|
+
* Set to false to keep the message on one line instead of wrapping it.
|
|
29
|
+
* @param {number} options.wrapWidth
|
|
30
|
+
* Column width at which the message wraps onto a new line.
|
|
31
|
+
* @returns {string}
|
|
32
|
+
* Rendered labelled line.
|
|
33
|
+
*/
|
|
34
|
+
export function labelledLine(label = "", message = "", options = {}) {
|
|
35
|
+
const prefixText = createPrefix(label, options);
|
|
36
|
+
const prefix = renderPrefix(prefixText, options);
|
|
37
|
+
const messageText = String(message);
|
|
38
|
+
|
|
39
|
+
if (prefix === "") {
|
|
40
|
+
return messageText;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (options.wrap === false) {
|
|
44
|
+
return `${prefix}${messageSeparator}${messageText}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const wrapWidth = resolveWrapWidth(prefixText, options);
|
|
48
|
+
const messageLines = wrapText(messageText, wrapWidth);
|
|
49
|
+
const continuationIndent = " ".repeat(prefixText.length + messageSeparator.length);
|
|
50
|
+
const wrappedMessage = messageLines.join(`\n${continuationIndent}`);
|
|
51
|
+
|
|
52
|
+
return `${prefix}${messageSeparator}${wrappedMessage}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Colour the icon/label prefix, sized from its plain-text length so the
|
|
57
|
+
* continuation indent lines up whether or not colour is applied.
|
|
58
|
+
*
|
|
59
|
+
* @param {string} prefix
|
|
60
|
+
* Plain-text icon and label prefix.
|
|
61
|
+
* @param {object} options
|
|
62
|
+
* Rendering options.
|
|
63
|
+
* @returns {string}
|
|
64
|
+
* Prefix, coloured when requested.
|
|
65
|
+
*/
|
|
66
|
+
function renderPrefix(prefix, options) {
|
|
67
|
+
if (prefix === "") {
|
|
68
|
+
return prefix;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (options.colour !== true) {
|
|
72
|
+
return prefix;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const colouredPrefix = foreground(prefix, resolveColour(options), options);
|
|
76
|
+
|
|
77
|
+
return style(colouredPrefix, "bold", options);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Build the plain-text icon and label prefix.
|
|
82
|
+
*
|
|
83
|
+
* @param {string} label
|
|
84
|
+
* Label text.
|
|
85
|
+
* @param {object} options
|
|
86
|
+
* Rendering options.
|
|
87
|
+
* @returns {string}
|
|
88
|
+
* Icon and label joined with a colon and space, or an empty string when both are empty.
|
|
89
|
+
*/
|
|
90
|
+
function createPrefix(label, options) {
|
|
91
|
+
const labelText = String(label);
|
|
92
|
+
const icon = typeof options.icon === "string" ? options.icon : "";
|
|
93
|
+
const labelPart = labelText === "" ? "" : `${labelText}:`;
|
|
94
|
+
|
|
95
|
+
return [icon, labelPart].filter((part) => part !== "").join(" ");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Pick the prefix's colour token.
|
|
100
|
+
*
|
|
101
|
+
* @param {object} options
|
|
102
|
+
* Rendering options.
|
|
103
|
+
* @returns {string}
|
|
104
|
+
* Colour token, defaulting to "info" when no tone is given.
|
|
105
|
+
*/
|
|
106
|
+
function resolveColour(options) {
|
|
107
|
+
if (typeof options.tone === "string" && options.tone !== "") {
|
|
108
|
+
return options.tone;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return defaultColour;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Work out how wide the message can be before it wraps.
|
|
116
|
+
*
|
|
117
|
+
* @param {string} prefix
|
|
118
|
+
* Plain-text icon and label prefix, used to size the continuation indent.
|
|
119
|
+
* @param {object} options
|
|
120
|
+
* Rendering options.
|
|
121
|
+
* @returns {number|undefined}
|
|
122
|
+
* Explicit or derived wrap width, or undefined to use wrapText's own default.
|
|
123
|
+
*/
|
|
124
|
+
function resolveWrapWidth(prefix, options) {
|
|
125
|
+
if (options.wrapWidth !== undefined) {
|
|
126
|
+
return options.wrapWidth;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (Number.isFinite(options.width)) {
|
|
130
|
+
return Math.max(1, options.width - prefix.length - messageSeparator.length);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
package/src/primitives/row.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { foreground } from "../formatters/ansi.js";
|
|
2
|
+
import { wrapText } from "../formatters/wrap.js";
|
|
2
3
|
import { getResultSymbol, getResultToken } from "../theme/results.js";
|
|
3
4
|
|
|
4
5
|
// Row labels use the same subdued tone as table headers by default.
|
|
@@ -7,6 +8,9 @@ const defaultLabelColour = "muted";
|
|
|
7
8
|
// Rows use a compact gap that still reads cleanly in plain terminals.
|
|
8
9
|
const defaultSeparator = " ";
|
|
9
10
|
|
|
11
|
+
// Keeps a wrapped value within a typical narrow terminal column.
|
|
12
|
+
const defaultWrapWidth = 64;
|
|
13
|
+
|
|
10
14
|
/**
|
|
11
15
|
* Render a labelled value row.
|
|
12
16
|
*
|
|
@@ -28,6 +32,12 @@ const defaultSeparator = " ";
|
|
|
28
32
|
* Text between label and value.
|
|
29
33
|
* @param {string} options.valueColour
|
|
30
34
|
* Optional colour token for the value.
|
|
35
|
+
* @param {number} options.width
|
|
36
|
+
* Total row width used to calculate the available value width.
|
|
37
|
+
* @param {boolean} options.wrap
|
|
38
|
+
* Set to false to keep the value on one line instead of wrapping it.
|
|
39
|
+
* @param {number} options.wrapWidth
|
|
40
|
+
* Column width at which a value wraps onto a new line.
|
|
31
41
|
* @returns {string}
|
|
32
42
|
* Rendered row.
|
|
33
43
|
*/
|
|
@@ -45,7 +55,22 @@ export function row(label, value = "", options = {}) {
|
|
|
45
55
|
const renderedValue = renderValue(value, options);
|
|
46
56
|
const separator = options.separator ?? defaultSeparator;
|
|
47
57
|
|
|
48
|
-
|
|
58
|
+
if (options.wrap === false) {
|
|
59
|
+
return `${renderedLabel}${separator}${renderedValue}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const wrapWidth =
|
|
63
|
+
options.wrapWidth ??
|
|
64
|
+
(Number.isFinite(options.width)
|
|
65
|
+
? Math.max(1, options.width - labelWidth - separator.length)
|
|
66
|
+
: defaultWrapWidth);
|
|
67
|
+
|
|
68
|
+
const valueLines = wrapText(value, wrapWidth);
|
|
69
|
+
const continuationIndent = " ".repeat(labelWidth + separator.length);
|
|
70
|
+
const renderedValueLines = valueLines.map((line) => renderValue(line, options));
|
|
71
|
+
const wrappedValue = renderedValueLines.join(`\n${continuationIndent}`);
|
|
72
|
+
|
|
73
|
+
return `${renderedLabel}${separator}${wrappedValue}`;
|
|
49
74
|
}
|
|
50
75
|
|
|
51
76
|
/**
|