@lewishowles/cli-style 0.14.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 +19 -0
- package/README.md +26 -16
- package/adapters/python/LICENSE +21 -0
- package/adapters/python/README.md +29 -0
- package/adapters/python/cli_style/_wrappers.py +3 -3
- package/adapters/python/pyproject.toml +19 -2
- package/adapters/swift/Sources/CliStyle/Wrappers.swift +3 -3
- package/package.json +1 -1
- package/scripts/build-binary.sh +3 -1
- package/scripts/generate-adapters.js +80 -8
- package/scripts/package-binary.sh +2 -0
- 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,25 @@
|
|
|
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
|
+
|
|
16
|
+
## 0.15.0 - 2026-09-20
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- The Python adapter is published to PyPI as `lewishowles-cli-style`, so Python projects can install it with `pip install lewishowles-cli-style` instead of from a local path or this repository. The name it is imported under is unchanged: `import cli_style`.
|
|
21
|
+
- `bun run check:adapters` now fails when `package.json` and the Python package declare different versions, or when the Python package's copy of the licence differs from the one at the repository root.
|
|
22
|
+
- Releasing a tag whose version does not match `package.json` now fails before anything is published.
|
|
23
|
+
|
|
5
24
|
## 0.14.0 - 2026-09-20
|
|
6
25
|
|
|
7
26
|
### Added
|
package/README.md
CHANGED
|
@@ -17,16 +17,22 @@ JavaScript projects can install the package directly:
|
|
|
17
17
|
bun add @lewishowles/cli-style
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
-
Python projects can install the
|
|
20
|
+
Python projects can install the published package from PyPI:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install lewishowles-cli-style
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
They can also install the adapter from a local path with `uv`:
|
|
21
27
|
|
|
22
28
|
```bash
|
|
23
29
|
uv add --editable ../cli-style/adapters/python
|
|
24
30
|
```
|
|
25
31
|
|
|
26
|
-
|
|
32
|
+
Or install it straight from this repository:
|
|
27
33
|
|
|
28
34
|
```bash
|
|
29
|
-
uv add "cli-style @ git+https://github.com/lewishowles/cli-style.git#subdirectory=adapters/python"
|
|
35
|
+
uv add "lewishowles-cli-style @ git+https://github.com/lewishowles/cli-style.git#subdirectory=adapters/python"
|
|
30
36
|
```
|
|
31
37
|
|
|
32
38
|
Swift projects can add `adapters/swift` as a local package dependency and use its `CliStyle` product.
|
|
@@ -80,6 +86,10 @@ ui.print(output);
|
|
|
80
86
|
|
|
81
87
|
Renderer methods return strings. `ui.print()`, `ui.write()`, and CLI commands handle stdout and stderr.
|
|
82
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
|
+
|
|
83
93
|
### CLI render
|
|
84
94
|
|
|
85
95
|
Use `cli-style render <renderer>` when a script needs rendered text from JSON input:
|
|
@@ -377,19 +387,19 @@ Pre-render a `span` when only part of the hint needs emphasis.
|
|
|
377
387
|
|
|
378
388
|
### Structured primitives
|
|
379
389
|
|
|
380
|
-
| Renderer | Main fields
|
|
381
|
-
| --------------- |
|
|
382
|
-
| `divider` | `label`, `dividerWidth`, `character`
|
|
383
|
-
| `chip` | `label`, `tone`
|
|
384
|
-
| `panel` | `title`, `lines`, `tone`, `
|
|
385
|
-
| `table` | `columns`, `rows`, `width`
|
|
386
|
-
| `progress-bar` | `value`, `max`, `barWidth`, `tone`
|
|
387
|
-
| `bar-chart` | `rows`, `barWidth`
|
|
388
|
-
| `sparkline` | `values`, `label`, `tone`, `width`
|
|
389
|
-
| `step` | `label`, `state`
|
|
390
|
-
| `step-progress` | `steps`, `current`
|
|
391
|
-
| `empty-state` | `title`, `detail`
|
|
392
|
-
| `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` |
|
|
393
403
|
|
|
394
404
|
For visual examples, run `cli-style gallery --section primitives`.
|
|
395
405
|
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Lewis Howles
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# lewishowles-cli-style
|
|
2
|
+
|
|
3
|
+
`lewishowles-cli-style` is a Python adapter for the shared `cli-style` command-line renderer. It lets Python programs use the same terminal output styles as the JavaScript package.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
python -m pip install lewishowles-cli-style
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
The package supports Python 3.9 and later. The installed distribution is named `lewishowles-cli-style`, and you import it as `cli_style`.
|
|
12
|
+
|
|
13
|
+
Rendering happens in the separate `cli-style` binary, so that needs to be on your `PATH`. The [main readme](https://github.com/lewishowles/cli-style#install) covers the ways to install it.
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
from cli_style import render
|
|
19
|
+
|
|
20
|
+
print(render("status", {"type": "success", "label": "Build passed"}))
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
If the `cli-style` binary is not available, `render()` returns sorted plain `key: value` text by default. Pass `raise_on_missing=True` to raise `CliStyleNotFoundError` instead.
|
|
24
|
+
|
|
25
|
+
## More information
|
|
26
|
+
|
|
27
|
+
- [Repository](https://github.com/lewishowles/cli-style)
|
|
28
|
+
- [Issue tracker](https://github.com/lewishowles/cli-style/issues)
|
|
29
|
+
- [MIT licence](https://github.com/lewishowles/cli-style/blob/main/LICENSE)
|
|
@@ -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
|
|
|
@@ -1,11 +1,28 @@
|
|
|
1
1
|
[project]
|
|
2
|
-
name = "cli-style"
|
|
3
|
-
version = "0.
|
|
2
|
+
name = "lewishowles-cli-style"
|
|
3
|
+
version = "0.16.0"
|
|
4
4
|
description = "Python adapter for the shared cli-style binary."
|
|
5
5
|
authors = [{ name = "Lewis Howles" }]
|
|
6
6
|
license = { text = "MIT" }
|
|
7
|
+
license-files = ["LICENSE"]
|
|
8
|
+
readme = "README.md"
|
|
7
9
|
requires-python = ">=3.9"
|
|
8
10
|
dependencies = []
|
|
11
|
+
classifiers = [
|
|
12
|
+
"License :: OSI Approved :: MIT License",
|
|
13
|
+
"Programming Language :: Python :: 3",
|
|
14
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
15
|
+
"Programming Language :: Python :: 3.9",
|
|
16
|
+
"Programming Language :: Python :: 3.10",
|
|
17
|
+
"Programming Language :: Python :: 3.11",
|
|
18
|
+
"Programming Language :: Python :: 3.12",
|
|
19
|
+
"Programming Language :: Python :: 3.13",
|
|
20
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[project.urls]
|
|
24
|
+
Repository = "https://github.com/lewishowles/cli-style"
|
|
25
|
+
Issues = "https://github.com/lewishowles/cli-style/issues"
|
|
9
26
|
|
|
10
27
|
[build-system]
|
|
11
28
|
requires = ["hatchling"]
|
|
@@ -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
package/scripts/build-binary.sh
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
# dist/adapters/bash/cli-style.sh
|
|
7
7
|
# dist/adapters/python/cli_style/
|
|
8
8
|
# dist/adapters/python/pyproject.toml
|
|
9
|
+
# dist/adapters/python/README.md
|
|
10
|
+
# dist/adapters/python/LICENSE
|
|
9
11
|
# dist/adapters/swift/Package.swift
|
|
10
12
|
# dist/adapters/swift/Sources/
|
|
11
13
|
|
|
@@ -29,6 +31,6 @@ cp adapters/bash/cli-style.sh dist/adapters/bash/cli-style.sh
|
|
|
29
31
|
# Copy the Python package without the __pycache__ folders Python leaves behind.
|
|
30
32
|
tar -C "adapters/python" \
|
|
31
33
|
--exclude="__pycache__" \
|
|
32
|
-
-cf - cli_style pyproject.toml | tar -C "dist/adapters/python" -xf -
|
|
34
|
+
-cf - cli_style pyproject.toml README.md LICENSE | tar -C "dist/adapters/python" -xf -
|
|
33
35
|
|
|
34
36
|
cp -R adapters/swift/Package.swift adapters/swift/Sources dist/adapters/swift/
|
|
@@ -4,16 +4,19 @@ import { fileURLToPath } from "node:url";
|
|
|
4
4
|
|
|
5
5
|
import { rendererCatalogue } from "../src/catalogue/renderer-catalogue.js";
|
|
6
6
|
|
|
7
|
+
const scriptsDirectoryPath = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const repositoryRootPath = join(scriptsDirectoryPath, "..");
|
|
9
|
+
|
|
7
10
|
// Generated adapter modules are kept beside their hand-written language cores.
|
|
8
|
-
const pythonOutputPath = join(
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
);
|
|
11
|
+
const pythonOutputPath = join(repositoryRootPath, "adapters/python/cli_style/_wrappers.py");
|
|
12
|
+
|
|
13
|
+
const swiftOutputPath = join(repositoryRootPath, "adapters/swift/Sources/CliStyle/Wrappers.swift");
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
);
|
|
15
|
+
// Package metadata and licence files checked alongside generated adapter modules.
|
|
16
|
+
const packageJsonPath = join(repositoryRootPath, "package.json");
|
|
17
|
+
const pythonProjectPath = join(repositoryRootPath, "adapters/python/pyproject.toml");
|
|
18
|
+
const repositoryLicensePath = join(repositoryRootPath, "LICENSE");
|
|
19
|
+
const pythonLicensePath = join(repositoryRootPath, "adapters/python/LICENSE");
|
|
17
20
|
|
|
18
21
|
// Map catalogue types to the closest useful Python annotations.
|
|
19
22
|
const pythonTypes = {
|
|
@@ -379,10 +382,59 @@ const generatedFiles = [
|
|
|
379
382
|
},
|
|
380
383
|
];
|
|
381
384
|
|
|
385
|
+
/**
|
|
386
|
+
* Read a licence file, treating an absent file as a reportable state rather than a crash.
|
|
387
|
+
*
|
|
388
|
+
* @param {string} path
|
|
389
|
+
* The licence file to read.
|
|
390
|
+
* @returns {Buffer|null}
|
|
391
|
+
* The file contents, or null when the file is not there.
|
|
392
|
+
*/
|
|
393
|
+
function readLicenseFile(path) {
|
|
394
|
+
try {
|
|
395
|
+
return readFileSync(path);
|
|
396
|
+
} catch (error) {
|
|
397
|
+
if (error.code === "ENOENT") {
|
|
398
|
+
return null;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
throw error;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
382
405
|
// `--check` reports committed wrappers that differ from the generated source and writes nothing.
|
|
383
406
|
const checkOnly = process.argv.includes("--check");
|
|
384
407
|
|
|
385
408
|
if (checkOnly) {
|
|
409
|
+
// Both packages are released from one tag, so a version edited in one file and
|
|
410
|
+
// forgotten in the other would publish a Python package claiming a version that
|
|
411
|
+
// was never released.
|
|
412
|
+
const packageManifest = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
413
|
+
const packageVersion = packageManifest.version;
|
|
414
|
+
|
|
415
|
+
// The version is the only field read out of pyproject.toml, and the file is ours, so
|
|
416
|
+
// a pattern match avoids taking on a TOML parser for one line.
|
|
417
|
+
const pythonVersionMatch = readFileSync(pythonProjectPath, "utf8").match(
|
|
418
|
+
/^version\s*=\s*"([^"]+)"/m,
|
|
419
|
+
);
|
|
420
|
+
|
|
421
|
+
const pythonVersion = pythonVersionMatch?.[1];
|
|
422
|
+
const versionsMatch = packageVersion === pythonVersion;
|
|
423
|
+
|
|
424
|
+
// The Python package carries its own copy of the licence, because a wheel built from
|
|
425
|
+
// the published source archive cannot reach the one at the repository root.
|
|
426
|
+
const repositoryLicense = readLicenseFile(repositoryLicensePath);
|
|
427
|
+
const pythonLicense = readLicenseFile(pythonLicensePath);
|
|
428
|
+
|
|
429
|
+
const missingLicensePaths = [
|
|
430
|
+
["LICENSE", repositoryLicense],
|
|
431
|
+
["adapters/python/LICENSE", pythonLicense],
|
|
432
|
+
]
|
|
433
|
+
.filter(([, contents]) => contents === null)
|
|
434
|
+
.map(([path]) => path);
|
|
435
|
+
|
|
436
|
+
const licensesMatch = missingLicensePaths.length === 0 && repositoryLicense.equals(pythonLicense);
|
|
437
|
+
|
|
386
438
|
// A committed wrapper is stale when its contents differ from the generated source; a missing file counts as stale.
|
|
387
439
|
const staleFiles = generatedFiles.filter(({ path, source }) => {
|
|
388
440
|
try {
|
|
@@ -396,13 +448,33 @@ if (checkOnly) {
|
|
|
396
448
|
}
|
|
397
449
|
});
|
|
398
450
|
|
|
451
|
+
if (!versionsMatch) {
|
|
452
|
+
console.error("Package versions do not match:");
|
|
453
|
+
console.error(`- package.json: ${packageVersion ?? "missing"}`);
|
|
454
|
+
console.error(`- adapters/python/pyproject.toml: ${pythonVersion ?? "missing"}`);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (missingLicensePaths.length > 0) {
|
|
458
|
+
console.error("Licence files are missing:");
|
|
459
|
+
|
|
460
|
+
for (const path of missingLicensePaths) {
|
|
461
|
+
console.error(`- ${path}`);
|
|
462
|
+
}
|
|
463
|
+
} else if (!licensesMatch) {
|
|
464
|
+
console.error("Licence files do not match:");
|
|
465
|
+
console.error("- LICENSE");
|
|
466
|
+
console.error("- adapters/python/LICENSE");
|
|
467
|
+
}
|
|
468
|
+
|
|
399
469
|
if (staleFiles.length > 0) {
|
|
400
470
|
console.error("Generated adapters are stale:");
|
|
401
471
|
|
|
402
472
|
for (const { path } of staleFiles) {
|
|
403
473
|
console.error(`- ${path}`);
|
|
404
474
|
}
|
|
475
|
+
}
|
|
405
476
|
|
|
477
|
+
if (staleFiles.length > 0 || !versionsMatch || !licensesMatch) {
|
|
406
478
|
process.exitCode = 1;
|
|
407
479
|
}
|
|
408
480
|
} else {
|
|
@@ -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
|
/**
|