@lewishowles/cli-style 0.15.0 → 0.16.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 +11 -0
- package/README.md +17 -13
- package/adapters/python/cli_style/_wrappers.py +3 -3
- package/adapters/python/pyproject.toml +1 -1
- package/adapters/swift/Sources/CliStyle/Wrappers.swift +3 -3
- package/package.json +1 -1
- package/src/capability/terminal.js +16 -7
- package/src/catalogue/renderer-catalogue.js +1 -1
- package/src/cli/render-command.js +8 -0
- package/src/gallery/render-gallery.js +1 -2
- package/src/primitives/panel.js +27 -20
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.16.0 - 2026-09-21
|
|
6
|
+
|
|
7
|
+
### Breaking changes
|
|
8
|
+
|
|
9
|
+
- BREAKING: Removed the `panelWidth` option from `panel()` and the `panel` renderer. Use `width` instead.
|
|
10
|
+
- BREAKING: Panels now honour the requested total `width`, raise widths below 6 to the minimum usable width, and wrap long lines to fit.
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- An explicit JSON `width` now takes precedence over the terminal width for width-aware renderers, so piped output stays within the caller's requested bounds.
|
|
15
|
+
|
|
5
16
|
## 0.15.0 - 2026-09-20
|
|
6
17
|
|
|
7
18
|
### Added
|
package/README.md
CHANGED
|
@@ -86,6 +86,10 @@ ui.print(output);
|
|
|
86
86
|
|
|
87
87
|
Renderer methods return strings. `ui.print()`, `ui.write()`, and CLI commands handle stdout and stderr.
|
|
88
88
|
|
|
89
|
+
For `render` commands, width follows this priority: JSON payload `width`, `--width`,
|
|
90
|
+
`stdout.columns`, positive `COLUMNS`, then `80`, so piped output stays within the caller's requested
|
|
91
|
+
size when the payload supplies a width.
|
|
92
|
+
|
|
89
93
|
### CLI render
|
|
90
94
|
|
|
91
95
|
Use `cli-style render <renderer>` when a script needs rendered text from JSON input:
|
|
@@ -383,19 +387,19 @@ Pre-render a `span` when only part of the hint needs emphasis.
|
|
|
383
387
|
|
|
384
388
|
### Structured primitives
|
|
385
389
|
|
|
386
|
-
| Renderer | Main fields
|
|
387
|
-
| --------------- |
|
|
388
|
-
| `divider` | `label`, `dividerWidth`, `character`
|
|
389
|
-
| `chip` | `label`, `tone`
|
|
390
|
-
| `panel` | `title`, `lines`, `tone`, `
|
|
391
|
-
| `table` | `columns`, `rows`, `width`
|
|
392
|
-
| `progress-bar` | `value`, `max`, `barWidth`, `tone`
|
|
393
|
-
| `bar-chart` | `rows`, `barWidth`
|
|
394
|
-
| `sparkline` | `values`, `label`, `tone`, `width`
|
|
395
|
-
| `step` | `label`, `state`
|
|
396
|
-
| `step-progress` | `steps`, `current`
|
|
397
|
-
| `empty-state` | `title`, `detail`
|
|
398
|
-
| `error-block` | `title`, `lines`
|
|
390
|
+
| Renderer | Main fields |
|
|
391
|
+
| --------------- | ------------------------------------ |
|
|
392
|
+
| `divider` | `label`, `dividerWidth`, `character` |
|
|
393
|
+
| `chip` | `label`, `tone` |
|
|
394
|
+
| `panel` | `title`, `lines`, `tone`, `width` |
|
|
395
|
+
| `table` | `columns`, `rows`, `width` |
|
|
396
|
+
| `progress-bar` | `value`, `max`, `barWidth`, `tone` |
|
|
397
|
+
| `bar-chart` | `rows`, `barWidth` |
|
|
398
|
+
| `sparkline` | `values`, `label`, `tone`, `width` |
|
|
399
|
+
| `step` | `label`, `state` |
|
|
400
|
+
| `step-progress` | `steps`, `current` |
|
|
401
|
+
| `empty-state` | `title`, `detail` |
|
|
402
|
+
| `error-block` | `title`, `lines` |
|
|
399
403
|
|
|
400
404
|
For visual examples, run `cli-style gallery --section primitives`.
|
|
401
405
|
|
|
@@ -297,7 +297,7 @@ def panel(
|
|
|
297
297
|
lines: list[str] | None = None,
|
|
298
298
|
title: str | None = None,
|
|
299
299
|
tone: str = "info",
|
|
300
|
-
|
|
300
|
+
width: int | float | None = None,
|
|
301
301
|
**kwargs: Any,
|
|
302
302
|
) -> str:
|
|
303
303
|
"""Return panel output; extra keyword arguments go to render()."""
|
|
@@ -308,8 +308,8 @@ def panel(
|
|
|
308
308
|
|
|
309
309
|
if title is not None:
|
|
310
310
|
data["title"] = title
|
|
311
|
-
if
|
|
312
|
-
data["
|
|
311
|
+
if width is not None:
|
|
312
|
+
data["width"] = width
|
|
313
313
|
|
|
314
314
|
return render("panel", data, **kwargs)
|
|
315
315
|
|
|
@@ -341,7 +341,7 @@ extension CliStyle {
|
|
|
341
341
|
lines: [String]? = nil,
|
|
342
342
|
title: String? = nil,
|
|
343
343
|
tone: String = "info",
|
|
344
|
-
|
|
344
|
+
width: Int? = nil,
|
|
345
345
|
options: CliStyleOptions = .init()
|
|
346
346
|
) throws -> String {
|
|
347
347
|
var data: [String: Any] = [
|
|
@@ -352,8 +352,8 @@ extension CliStyle {
|
|
|
352
352
|
if let title = title {
|
|
353
353
|
data["title"] = title
|
|
354
354
|
}
|
|
355
|
-
if let
|
|
356
|
-
data["
|
|
355
|
+
if let width = width {
|
|
356
|
+
data["width"] = width
|
|
357
357
|
}
|
|
358
358
|
|
|
359
359
|
return try render("panel", data: data, options: options)
|
package/package.json
CHANGED
|
@@ -29,7 +29,7 @@ export function resolveTerminalCapabilities(options = {}) {
|
|
|
29
29
|
const isCi = env.CI === "true" || env.CI === "1";
|
|
30
30
|
const isDumb = env.TERM === "dumb";
|
|
31
31
|
const forceColour = env.FORCE_COLOR !== undefined && env.FORCE_COLOR !== "0";
|
|
32
|
-
const width = resolveWidth(stdout.columns);
|
|
32
|
+
const width = resolveWidth(stdout.columns, env.COLUMNS);
|
|
33
33
|
const usesPlain = argv.includes("--plain");
|
|
34
34
|
|
|
35
35
|
const colour = resolveColour({
|
|
@@ -189,16 +189,25 @@ function resolveUnicode(options) {
|
|
|
189
189
|
}
|
|
190
190
|
|
|
191
191
|
/**
|
|
192
|
-
* Resolve terminal width
|
|
192
|
+
* Resolve the terminal width from the output stream, then `COLUMNS`, then a fixed fallback.
|
|
193
193
|
*
|
|
194
|
-
* @param {number}
|
|
195
|
-
*
|
|
194
|
+
* @param {number} streamWidth
|
|
195
|
+
* The column count the output stream reports when it is a terminal.
|
|
196
|
+
* @param {string} columns
|
|
197
|
+
* The `COLUMNS` environment value, used when the stream reports no width, such as when
|
|
198
|
+
* output is piped. Anything other than a positive whole number is ignored.
|
|
196
199
|
* @returns {number}
|
|
197
200
|
* Usable terminal width.
|
|
198
201
|
*/
|
|
199
|
-
function resolveWidth(
|
|
200
|
-
if (Number.isInteger(
|
|
201
|
-
return
|
|
202
|
+
function resolveWidth(streamWidth, columns) {
|
|
203
|
+
if (Number.isInteger(streamWidth) && streamWidth > 0) {
|
|
204
|
+
return streamWidth;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const environmentWidth = Number(columns);
|
|
208
|
+
|
|
209
|
+
if (Number.isInteger(environmentWidth) && environmentWidth > 0) {
|
|
210
|
+
return environmentWidth;
|
|
202
211
|
}
|
|
203
212
|
|
|
204
213
|
return defaultWidth;
|
|
@@ -38,6 +38,7 @@ const renderers = {
|
|
|
38
38
|
bulletList(input.items, {
|
|
39
39
|
...input,
|
|
40
40
|
...options,
|
|
41
|
+
width: input.width ?? options.width,
|
|
41
42
|
}),
|
|
42
43
|
chip: (input, options) =>
|
|
43
44
|
chip(input.label, input.tone, {
|
|
@@ -63,6 +64,7 @@ const renderers = {
|
|
|
63
64
|
errorBlock(input.title, input.lines, {
|
|
64
65
|
...input,
|
|
65
66
|
...options,
|
|
67
|
+
width: input.width ?? options.width,
|
|
66
68
|
}),
|
|
67
69
|
hint: (input, options) =>
|
|
68
70
|
hint(input.message, {
|
|
@@ -73,12 +75,14 @@ const renderers = {
|
|
|
73
75
|
labelledLine(input.label, input.message, {
|
|
74
76
|
...input,
|
|
75
77
|
...options,
|
|
78
|
+
width: input.width ?? options.width,
|
|
76
79
|
}),
|
|
77
80
|
"next-step-block": (input, options) => nextStepBlock(input, options),
|
|
78
81
|
panel: (input, options) =>
|
|
79
82
|
panel({
|
|
80
83
|
...input,
|
|
81
84
|
...options,
|
|
85
|
+
width: input.width ?? options.width,
|
|
82
86
|
}),
|
|
83
87
|
"progress-bar": (input, options) =>
|
|
84
88
|
progressBar({
|
|
@@ -89,16 +93,19 @@ const renderers = {
|
|
|
89
93
|
row(input.label, input.value, {
|
|
90
94
|
...input,
|
|
91
95
|
...options,
|
|
96
|
+
width: input.width ?? options.width,
|
|
92
97
|
}),
|
|
93
98
|
"row-group": (input, options) =>
|
|
94
99
|
rowGroup({
|
|
95
100
|
...input,
|
|
96
101
|
...options,
|
|
102
|
+
width: input.width ?? options.width,
|
|
97
103
|
}),
|
|
98
104
|
sparkline: (input, options) =>
|
|
99
105
|
sparkline({
|
|
100
106
|
...input,
|
|
101
107
|
...options,
|
|
108
|
+
width: input.width ?? options.width,
|
|
102
109
|
}),
|
|
103
110
|
span: (input, options) =>
|
|
104
111
|
span(input.value, input.tone, {
|
|
@@ -124,6 +131,7 @@ const renderers = {
|
|
|
124
131
|
table({
|
|
125
132
|
...input,
|
|
126
133
|
...options,
|
|
134
|
+
width: input.width ?? options.width,
|
|
127
135
|
}),
|
|
128
136
|
"task-summary": (input, options) => taskSummary(input, options),
|
|
129
137
|
};
|
|
@@ -280,7 +280,6 @@ function galleryWidthOptions(width) {
|
|
|
280
280
|
return {
|
|
281
281
|
barWidth: Math.max(width - 18, 1),
|
|
282
282
|
dividerWidth: width,
|
|
283
|
-
panelWidth: width,
|
|
284
283
|
};
|
|
285
284
|
}
|
|
286
285
|
|
|
@@ -790,7 +789,7 @@ function renderPrimitives(options) {
|
|
|
790
789
|
emptyState("No results", "tone: muted", options),
|
|
791
790
|
errorBlock("Failed", ["tone: danger"], {
|
|
792
791
|
...options,
|
|
793
|
-
|
|
792
|
+
width: 40,
|
|
794
793
|
}),
|
|
795
794
|
"",
|
|
796
795
|
"Steps",
|
package/src/primitives/panel.js
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { background, foreground, style } from "../formatters/ansi.js";
|
|
2
|
+
import { wrapText } from "../formatters/wrap.js";
|
|
2
3
|
import { profiles } from "../profiles/profiles.js";
|
|
3
4
|
import { panelColours } from "../theme/colours.js";
|
|
4
5
|
|
|
5
|
-
//
|
|
6
|
+
// The width used when no width is given, such as a direct library call outside a terminal.
|
|
6
7
|
const defaultWidth = 40;
|
|
7
8
|
|
|
9
|
+
// The narrowest panel that still leaves one column for content beside the accent and padding.
|
|
10
|
+
const minimumWidth = 6;
|
|
11
|
+
|
|
8
12
|
// Info is the neutral semantic accent for grouped explanatory content.
|
|
9
13
|
const defaultTone = "info";
|
|
10
14
|
|
|
@@ -17,8 +21,9 @@ const defaultTone = "info";
|
|
|
17
21
|
* Whether ANSI colour should be applied.
|
|
18
22
|
* @param {string[]} options.lines
|
|
19
23
|
* Content lines to render.
|
|
20
|
-
* @param {number} options.
|
|
21
|
-
* Total panel width.
|
|
24
|
+
* @param {number} options.width
|
|
25
|
+
* Total panel width, including the accent and padding. Widths below 6 are raised to 6,
|
|
26
|
+
* which leaves room for one character of content. Longer lines wrap to fit.
|
|
22
27
|
* @param {string} options.profile
|
|
23
28
|
* Active output profile.
|
|
24
29
|
* @param {string} options.title
|
|
@@ -37,18 +42,18 @@ export function panel(options = {}) {
|
|
|
37
42
|
return [...(options.title === undefined ? [] : [options.title]), ...lines].join("\n");
|
|
38
43
|
}
|
|
39
44
|
|
|
40
|
-
const width = Math.max(options.
|
|
45
|
+
const width = Math.max(options.width ?? defaultWidth, minimumWidth);
|
|
41
46
|
const panelLines = ["", ...(options.title === undefined ? [] : [options.title]), ...lines, ""];
|
|
42
47
|
|
|
43
48
|
return panelLines
|
|
44
|
-
.
|
|
49
|
+
.flatMap((line, index) =>
|
|
45
50
|
renderLine(line, width, index === 1 && options.title !== undefined, options),
|
|
46
51
|
)
|
|
47
52
|
.join("\n");
|
|
48
53
|
}
|
|
49
54
|
|
|
50
55
|
/**
|
|
51
|
-
* Render one
|
|
56
|
+
* Render one content line, wrapped across as many panel lines as it needs.
|
|
52
57
|
*
|
|
53
58
|
* @param {string} line
|
|
54
59
|
* Content line.
|
|
@@ -58,28 +63,30 @@ export function panel(options = {}) {
|
|
|
58
63
|
* Whether the line is the panel title.
|
|
59
64
|
* @param {object} options
|
|
60
65
|
* Rendering options.
|
|
61
|
-
* @returns {string}
|
|
62
|
-
*
|
|
66
|
+
* @returns {string[]}
|
|
67
|
+
* The panel lines, each exactly `width` columns wide.
|
|
63
68
|
*/
|
|
64
69
|
function renderLine(line, width, isTitle, options) {
|
|
65
70
|
const accent = options.unicode === false ? "|" : "▌";
|
|
66
71
|
const contentWidth = width - 5;
|
|
67
|
-
const visibleLine = line.slice(0, contentWidth);
|
|
68
|
-
const content = ` ${visibleLine.padEnd(contentWidth, " ")} `;
|
|
69
72
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
+
return wrapText(line, contentWidth).map((visibleLine) => {
|
|
74
|
+
const content = ` ${visibleLine.padEnd(contentWidth, " ")} `;
|
|
75
|
+
|
|
76
|
+
if (options.colour !== true) {
|
|
77
|
+
return `${accent}${content}`;
|
|
78
|
+
}
|
|
73
79
|
|
|
74
|
-
|
|
80
|
+
const renderedAccent = renderPanelColour(accent, options);
|
|
75
81
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
82
|
+
const renderedContent = foreground(
|
|
83
|
+
background(content, panelColours.background, options),
|
|
84
|
+
panelColours.body,
|
|
85
|
+
options,
|
|
86
|
+
);
|
|
81
87
|
|
|
82
|
-
|
|
88
|
+
return `${renderedAccent}${isTitle ? style(renderedContent, "bold", options) : renderedContent}`;
|
|
89
|
+
});
|
|
83
90
|
}
|
|
84
91
|
|
|
85
92
|
/**
|