@lewishowles/cli-style 0.15.0 → 0.17.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 +18 -0
- package/README.md +39 -14
- package/adapters/python/README.md +14 -0
- package/adapters/python/cli_style/__init__.py +2 -0
- package/adapters/python/cli_style/_wrappers.py +58 -4
- package/adapters/python/pyproject.toml +1 -1
- package/adapters/swift/Sources/CliStyle/Wrappers.swift +50 -3
- package/package.json +1 -1
- package/scripts/generate-adapters.js +25 -4
- package/src/capability/terminal.js +16 -7
- package/src/catalogue/renderer-catalogue.js +45 -5
- package/src/cli/render-command.js +10 -0
- package/src/gallery/render-gallery.js +19 -2
- package/src/index.js +1 -0
- package/src/patterns/quote-block.js +70 -0
- package/src/primitives/panel.js +27 -20
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.17.0 - 2026-10-03
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added `quoteBlock()` (`quote-block` for `cli-style render`, `quote_block()` in Python, and `CliStyle.quoteBlock` in Swift) to show another program's output with a visible marker and optional heading.
|
|
10
|
+
- Python `row()` and `row_group()`, and Swift `CliStyle.row` and `CliStyle.rowGroup`, can now wrap long values with the `wrap` and `wrap_width` options (`wrapWidth` in Swift). The row group functions also accept colour and separator defaults, which each row can override.
|
|
11
|
+
|
|
12
|
+
## 0.16.0 - 2026-09-21
|
|
13
|
+
|
|
14
|
+
### Breaking changes
|
|
15
|
+
|
|
16
|
+
- BREAKING: Removed the `panelWidth` option from `panel()` and the `panel` renderer. Use `width` instead.
|
|
17
|
+
- BREAKING: Panels now honour the requested total `width`, raise widths below 6 to the minimum usable width, and wrap long lines to fit.
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- 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.
|
|
22
|
+
|
|
5
23
|
## 0.15.0 - 2026-09-20
|
|
6
24
|
|
|
7
25
|
### Added
|
package/README.md
CHANGED
|
@@ -63,6 +63,8 @@ The package contains `bin/cli-style` and `adapters/`.
|
|
|
63
63
|
|
|
64
64
|
All renderers return text. JavaScript imports functions directly. CLI, Bash, Python, and Swift integrations pass JSON-compatible data to a named renderer.
|
|
65
65
|
|
|
66
|
+
Callers add any blank lines around rendered blocks. Renderers return strings without a trailing newline, and the CLI ends its output with one newline.
|
|
67
|
+
|
|
66
68
|
### JavaScript
|
|
67
69
|
|
|
68
70
|
Create the `ui` instance at the CLI entrypoint, then pass `ui.options` to renderers. That keeps flag, environment, stream, colour, Unicode, and width detection in one place.
|
|
@@ -86,6 +88,10 @@ ui.print(output);
|
|
|
86
88
|
|
|
87
89
|
Renderer methods return strings. `ui.print()`, `ui.write()`, and CLI commands handle stdout and stderr.
|
|
88
90
|
|
|
91
|
+
For `render` commands, width follows this priority: JSON payload `width`, `--width`,
|
|
92
|
+
`stdout.columns`, positive `COLUMNS`, then `80`, so piped output stays within the caller's requested
|
|
93
|
+
size when the payload supplies a width.
|
|
94
|
+
|
|
89
95
|
### CLI render
|
|
90
96
|
|
|
91
97
|
Use `cli-style render <renderer>` when a script needs rendered text from JSON input:
|
|
@@ -276,6 +282,7 @@ Renderer names are stable for `cli-style render`, `cli_style_render`, and Python
|
|
|
276
282
|
| `empty-state` | `emptyState(title, detail, options)` | Use `cli_style_render` | Use `render("empty-state", ...)` | Use `CliStyle.render` | Empty result message. |
|
|
277
283
|
| `error-block` | `errorBlock(title, lines, options)` | Use `cli_style_render` | Use `render("error-block", ...)` | Use `CliStyle.render` | Structured error block. |
|
|
278
284
|
| `diff-block` | `diffBlock(diff, options)` | Use `cli_style_render` | Use `render("diff-block", ...)` | Use `CliStyle.render` | Structured added, removed, context, and header lines. |
|
|
285
|
+
| `quote-block` | `quoteBlock(quote, options)` | Use `cli_style_render` | `quote_block(lines, title, width)` | `CliStyle.quoteBlock(...)` | Output lines with a left marker and optional heading. |
|
|
279
286
|
|
|
280
287
|
Use `render("<renderer>", data, ...)` (Python) or `CliStyle.render(_:data:options:)` (Swift) for aggregate patterns (`diagnostic-report`, `agent-transcript`, `compact-data-table`) or any renderer with multi-item fields.
|
|
281
288
|
|
|
@@ -313,6 +320,8 @@ Use these shapes with `cli-style render`, `cli_style_render`, or Python `render(
|
|
|
313
320
|
- `result`: optional result type for a symbol and tone
|
|
314
321
|
- `labelWidth`: optional minimum label width
|
|
315
322
|
- `labelColour`, `valueColour`, `separator`: optional presentation overrides
|
|
323
|
+
- `wrap`: set to `false` to keep the value on one line
|
|
324
|
+
- `wrapWidth`: optional value width when wrapping is enabled
|
|
316
325
|
|
|
317
326
|
### `row-group`
|
|
318
327
|
|
|
@@ -333,7 +342,10 @@ Use these shapes with `cli-style render`, `cli_style_render`, or Python `render(
|
|
|
333
342
|
|
|
334
343
|
- `rows`: labelled values to align and render
|
|
335
344
|
- `labelWidth`: optional minimum width for every label
|
|
336
|
-
-
|
|
345
|
+
- `labelColour`, `valueColour`, `separator`: optional defaults for every row
|
|
346
|
+
- `wrap`: set to `false` to keep every value on one line
|
|
347
|
+
- `wrapWidth`: optional value width when wrapping is enabled
|
|
348
|
+
- Each row takes the same fields as `row`. Row options other than `labelWidth` override the group defaults
|
|
337
349
|
|
|
338
350
|
### `bullet-list`
|
|
339
351
|
|
|
@@ -383,19 +395,19 @@ Pre-render a `span` when only part of the hint needs emphasis.
|
|
|
383
395
|
|
|
384
396
|
### Structured primitives
|
|
385
397
|
|
|
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`
|
|
398
|
+
| Renderer | Main fields |
|
|
399
|
+
| --------------- | ------------------------------------ |
|
|
400
|
+
| `divider` | `label`, `dividerWidth`, `character` |
|
|
401
|
+
| `chip` | `label`, `tone` |
|
|
402
|
+
| `panel` | `title`, `lines`, `tone`, `width` |
|
|
403
|
+
| `table` | `columns`, `rows`, `width` |
|
|
404
|
+
| `progress-bar` | `value`, `max`, `barWidth`, `tone` |
|
|
405
|
+
| `bar-chart` | `rows`, `barWidth` |
|
|
406
|
+
| `sparkline` | `values`, `label`, `tone`, `width` |
|
|
407
|
+
| `step` | `label`, `state` |
|
|
408
|
+
| `step-progress` | `steps`, `current` |
|
|
409
|
+
| `empty-state` | `title`, `detail` |
|
|
410
|
+
| `error-block` | `title`, `lines` |
|
|
399
411
|
|
|
400
412
|
For visual examples, run `cli-style gallery --section primitives`.
|
|
401
413
|
|
|
@@ -443,6 +455,19 @@ Patterns are built-in composite renderers for common report shapes. They use the
|
|
|
443
455
|
| `next-step-block` | `nextStepBlock(nextStep, options)` | Next-step guidance. | `title`, `next`, `reason`, `commands`, `alternatives` |
|
|
444
456
|
| `compact-data-table` | `compactDataTable(data, options)` | Titled compact data table. | `title`, `summary`, `columns`, `rows` |
|
|
445
457
|
| `diff-block` | `diffBlock(diff, options)` | Structured diff presentation. | `title`, `path`, `lines` |
|
|
458
|
+
| `quote-block` | `quoteBlock(quote, options)` | Quoted output with a visible marker. | `lines`, `title`, `width` |
|
|
459
|
+
|
|
460
|
+
### `quote-block`
|
|
461
|
+
|
|
462
|
+
Pass output lines with an optional title and total width. Long lines wrap with a marker beside every continuation line, and empty lines are kept. The marker is `│`, or `|` when Unicode is off, and it is muted when colour is enabled. Plain and CI output always use an uncoloured `|`. ANSI colour codes in the title and lines are removed. The `json` profile returns the input unchanged.
|
|
463
|
+
|
|
464
|
+
```js
|
|
465
|
+
quoteBlock({
|
|
466
|
+
lines: ["The command returned this explanation.", "A second output line."],
|
|
467
|
+
title: "Command output",
|
|
468
|
+
width: 48,
|
|
469
|
+
});
|
|
470
|
+
```
|
|
446
471
|
|
|
447
472
|
### `diff-block`
|
|
448
473
|
|
|
@@ -22,6 +22,20 @@ print(render("status", {"type": "success", "label": "Build passed"}))
|
|
|
22
22
|
|
|
23
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
24
|
|
|
25
|
+
Use `row()` for one labelled value or `row_group()` to align several values. Both accept `wrap=False` to keep values on one line and `wrap_width` to set the width used when wrapping. `row_group()` also accepts `label_colour`, `value_colour`, and `separator` as defaults for its rows:
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
from cli_style import row_group
|
|
29
|
+
|
|
30
|
+
print(row_group(
|
|
31
|
+
[{"label": "Command", "value": "python -m pip install lewishowles-cli-style"}],
|
|
32
|
+
wrap=False,
|
|
33
|
+
label_colour="muted",
|
|
34
|
+
))
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Each row has `label`, `value`, and an optional `result`. It can override the group defaults with `label_colour`, `value_colour`, `separator`, `wrap`, or `wrap_width`. The JavaScript spellings, such as `labelColour` and `wrapWidth`, also work in row dictionaries. Callers add any blank lines around rendered blocks. Python `render()` removes trailing newlines from rendered strings.
|
|
38
|
+
|
|
25
39
|
## More information
|
|
26
40
|
|
|
27
41
|
- [Repository](https://github.com/lewishowles/cli-style)
|
|
@@ -19,6 +19,7 @@ from ._wrappers import (
|
|
|
19
19
|
next_step_block,
|
|
20
20
|
panel,
|
|
21
21
|
progress_bar,
|
|
22
|
+
quote_block,
|
|
22
23
|
row,
|
|
23
24
|
row_group,
|
|
24
25
|
sparkline,
|
|
@@ -59,6 +60,7 @@ __all__ = [
|
|
|
59
60
|
"next_step_block",
|
|
60
61
|
"panel",
|
|
61
62
|
"progress_bar",
|
|
63
|
+
"quote_block",
|
|
62
64
|
"render",
|
|
63
65
|
"resolve_binary",
|
|
64
66
|
"row",
|
|
@@ -6,6 +6,22 @@ from typing import Any
|
|
|
6
6
|
|
|
7
7
|
from .core import render
|
|
8
8
|
|
|
9
|
+
|
|
10
|
+
def _camel_case_row(row: Any) -> Any:
|
|
11
|
+
"""Return a copy of a row with Python option names, such as label_colour, renamed to the camelCase names the renderer reads. Keys already in camelCase are kept, and a value that is not a dictionary is returned unchanged for the renderer to filter."""
|
|
12
|
+
if not isinstance(row, dict):
|
|
13
|
+
return row
|
|
14
|
+
|
|
15
|
+
converted_row = {}
|
|
16
|
+
|
|
17
|
+
for key, value in row.items():
|
|
18
|
+
parts = key.split("_")
|
|
19
|
+
camel_case_key = parts[0] + "".join(part.capitalize() for part in parts[1:])
|
|
20
|
+
converted_row[camel_case_key] = value
|
|
21
|
+
|
|
22
|
+
return converted_row
|
|
23
|
+
|
|
24
|
+
|
|
9
25
|
def agent_transcript(
|
|
10
26
|
entries: list[dict[str, Any]] | None = None,
|
|
11
27
|
title: str | None = None,
|
|
@@ -297,7 +313,7 @@ def panel(
|
|
|
297
313
|
lines: list[str] | None = None,
|
|
298
314
|
title: str | None = None,
|
|
299
315
|
tone: str = "info",
|
|
300
|
-
|
|
316
|
+
width: int | float | None = None,
|
|
301
317
|
**kwargs: Any,
|
|
302
318
|
) -> str:
|
|
303
319
|
"""Return panel output; extra keyword arguments go to render()."""
|
|
@@ -308,8 +324,8 @@ def panel(
|
|
|
308
324
|
|
|
309
325
|
if title is not None:
|
|
310
326
|
data["title"] = title
|
|
311
|
-
if
|
|
312
|
-
data["
|
|
327
|
+
if width is not None:
|
|
328
|
+
data["width"] = width
|
|
313
329
|
|
|
314
330
|
return render("panel", data, **kwargs)
|
|
315
331
|
|
|
@@ -334,6 +350,25 @@ def progress_bar(
|
|
|
334
350
|
return render("progress-bar", data, **kwargs)
|
|
335
351
|
|
|
336
352
|
|
|
353
|
+
def quote_block(
|
|
354
|
+
lines: list[str] | None = None,
|
|
355
|
+
title: str | None = None,
|
|
356
|
+
width: int | float | None = None,
|
|
357
|
+
**kwargs: Any,
|
|
358
|
+
) -> str:
|
|
359
|
+
"""Return quote-block output; extra keyword arguments go to render()."""
|
|
360
|
+
data = {
|
|
361
|
+
"lines": [] if lines is None else lines,
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if title is not None:
|
|
365
|
+
data["title"] = title
|
|
366
|
+
if width is not None:
|
|
367
|
+
data["width"] = width
|
|
368
|
+
|
|
369
|
+
return render("quote-block", data, **kwargs)
|
|
370
|
+
|
|
371
|
+
|
|
337
372
|
def row(
|
|
338
373
|
label: str,
|
|
339
374
|
value: str,
|
|
@@ -342,6 +377,8 @@ def row(
|
|
|
342
377
|
label_colour: str | None = None,
|
|
343
378
|
value_colour: str | None = None,
|
|
344
379
|
separator: str | None = None,
|
|
380
|
+
wrap: bool = True,
|
|
381
|
+
wrap_width: int | float | None = None,
|
|
345
382
|
**kwargs: Any,
|
|
346
383
|
) -> str:
|
|
347
384
|
"""Return row output; extra keyword arguments go to render()."""
|
|
@@ -349,6 +386,7 @@ def row(
|
|
|
349
386
|
"label": label,
|
|
350
387
|
"value": value,
|
|
351
388
|
"result": result,
|
|
389
|
+
"wrap": wrap,
|
|
352
390
|
}
|
|
353
391
|
|
|
354
392
|
if label_width is not None:
|
|
@@ -359,6 +397,8 @@ def row(
|
|
|
359
397
|
data["valueColour"] = value_colour
|
|
360
398
|
if separator is not None:
|
|
361
399
|
data["separator"] = separator
|
|
400
|
+
if wrap_width is not None:
|
|
401
|
+
data["wrapWidth"] = wrap_width
|
|
362
402
|
|
|
363
403
|
return render("row", data, **kwargs)
|
|
364
404
|
|
|
@@ -366,15 +406,29 @@ def row(
|
|
|
366
406
|
def row_group(
|
|
367
407
|
rows: list[dict[str, Any]],
|
|
368
408
|
label_width: int | float | None = None,
|
|
409
|
+
label_colour: str | None = None,
|
|
410
|
+
value_colour: str | None = None,
|
|
411
|
+
separator: str | None = None,
|
|
412
|
+
wrap: bool = True,
|
|
413
|
+
wrap_width: int | float | None = None,
|
|
369
414
|
**kwargs: Any,
|
|
370
415
|
) -> str:
|
|
371
416
|
"""Return row-group output; extra keyword arguments go to render()."""
|
|
372
417
|
data = {
|
|
373
|
-
"rows": rows,
|
|
418
|
+
"rows": [_camel_case_row(row) for row in rows],
|
|
419
|
+
"wrap": wrap,
|
|
374
420
|
}
|
|
375
421
|
|
|
376
422
|
if label_width is not None:
|
|
377
423
|
data["labelWidth"] = label_width
|
|
424
|
+
if label_colour is not None:
|
|
425
|
+
data["labelColour"] = label_colour
|
|
426
|
+
if value_colour is not None:
|
|
427
|
+
data["valueColour"] = value_colour
|
|
428
|
+
if separator is not None:
|
|
429
|
+
data["separator"] = separator
|
|
430
|
+
if wrap_width is not None:
|
|
431
|
+
data["wrapWidth"] = wrap_width
|
|
378
432
|
|
|
379
433
|
return render("row-group", data, **kwargs)
|
|
380
434
|
|
|
@@ -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)
|
|
@@ -382,6 +382,29 @@ extension CliStyle {
|
|
|
382
382
|
return try render("progress-bar", data: data, options: options)
|
|
383
383
|
}
|
|
384
384
|
|
|
385
|
+
/**
|
|
386
|
+
* Return quote-block output from the cli-style binary; `options` sets the profile, width, and binary.
|
|
387
|
+
*/
|
|
388
|
+
public static func quoteBlock(
|
|
389
|
+
lines: [String]? = nil,
|
|
390
|
+
title: String? = nil,
|
|
391
|
+
width: Int? = nil,
|
|
392
|
+
options: CliStyleOptions = .init()
|
|
393
|
+
) throws -> String {
|
|
394
|
+
var data: [String: Any] = [
|
|
395
|
+
"lines": lines ?? [],
|
|
396
|
+
]
|
|
397
|
+
|
|
398
|
+
if let title = title {
|
|
399
|
+
data["title"] = title
|
|
400
|
+
}
|
|
401
|
+
if let width = width {
|
|
402
|
+
data["width"] = width
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return try render("quote-block", data: data, options: options)
|
|
406
|
+
}
|
|
407
|
+
|
|
385
408
|
/**
|
|
386
409
|
* Return row output from the cli-style binary; `options` sets the profile, width, and binary.
|
|
387
410
|
*/
|
|
@@ -393,12 +416,15 @@ extension CliStyle {
|
|
|
393
416
|
labelColour: String? = nil,
|
|
394
417
|
valueColour: String? = nil,
|
|
395
418
|
separator: String? = nil,
|
|
419
|
+
wrap: Bool = true,
|
|
420
|
+
wrapWidth: Int? = nil,
|
|
396
421
|
options: CliStyleOptions = .init()
|
|
397
422
|
) throws -> String {
|
|
398
423
|
var data: [String: Any] = [
|
|
399
424
|
"label": label,
|
|
400
425
|
"value": value,
|
|
401
426
|
"result": result,
|
|
427
|
+
"wrap": wrap,
|
|
402
428
|
]
|
|
403
429
|
|
|
404
430
|
if let labelWidth = labelWidth {
|
|
@@ -413,6 +439,9 @@ extension CliStyle {
|
|
|
413
439
|
if let separator = separator {
|
|
414
440
|
data["separator"] = separator
|
|
415
441
|
}
|
|
442
|
+
if let wrapWidth = wrapWidth {
|
|
443
|
+
data["wrapWidth"] = wrapWidth
|
|
444
|
+
}
|
|
416
445
|
|
|
417
446
|
return try render("row", data: data, options: options)
|
|
418
447
|
}
|
|
@@ -423,15 +452,33 @@ extension CliStyle {
|
|
|
423
452
|
public static func rowGroup(
|
|
424
453
|
rows: [[String: Any]],
|
|
425
454
|
labelWidth: Int? = nil,
|
|
455
|
+
labelColour: String? = nil,
|
|
456
|
+
valueColour: String? = nil,
|
|
457
|
+
separator: String? = nil,
|
|
458
|
+
wrap: Bool = true,
|
|
459
|
+
wrapWidth: Int? = nil,
|
|
426
460
|
options: CliStyleOptions = .init()
|
|
427
461
|
) throws -> String {
|
|
428
462
|
var data: [String: Any] = [
|
|
429
463
|
"rows": rows,
|
|
464
|
+
"wrap": wrap,
|
|
430
465
|
]
|
|
431
466
|
|
|
432
467
|
if let labelWidth = labelWidth {
|
|
433
468
|
data["labelWidth"] = labelWidth
|
|
434
469
|
}
|
|
470
|
+
if let labelColour = labelColour {
|
|
471
|
+
data["labelColour"] = labelColour
|
|
472
|
+
}
|
|
473
|
+
if let valueColour = valueColour {
|
|
474
|
+
data["valueColour"] = valueColour
|
|
475
|
+
}
|
|
476
|
+
if let separator = separator {
|
|
477
|
+
data["separator"] = separator
|
|
478
|
+
}
|
|
479
|
+
if let wrapWidth = wrapWidth {
|
|
480
|
+
data["wrapWidth"] = wrapWidth
|
|
481
|
+
}
|
|
435
482
|
|
|
436
483
|
return try render("row-group", data: data, options: options)
|
|
437
484
|
}
|
package/package.json
CHANGED
|
@@ -235,10 +235,15 @@ function renderWrapper(renderer) {
|
|
|
235
235
|
(parameter) => !isRequiredParameter(parameter) && parameter.default === null,
|
|
236
236
|
);
|
|
237
237
|
|
|
238
|
-
const payloadLines = payloadParameters.map(
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
238
|
+
const payloadLines = payloadParameters.map((parameter) => {
|
|
239
|
+
const isRowGroupRows = renderer.name === "row-group" && parameter.name === "rows";
|
|
240
|
+
|
|
241
|
+
const payloadValue = isRowGroupRows
|
|
242
|
+
? "[_camel_case_row(row) for row in rows]"
|
|
243
|
+
: renderPayloadValue(parameter, descriptor);
|
|
244
|
+
|
|
245
|
+
return `\t\t${JSON.stringify(parameter.name)}: ${payloadValue},`;
|
|
246
|
+
});
|
|
242
247
|
|
|
243
248
|
const nullablePayloadLines = nullableParameters.flatMap((parameter) => {
|
|
244
249
|
const name = descriptor.parameterName(parameter);
|
|
@@ -279,6 +284,22 @@ function renderWrappers() {
|
|
|
279
284
|
"",
|
|
280
285
|
"from .core import render",
|
|
281
286
|
"",
|
|
287
|
+
"",
|
|
288
|
+
"def _camel_case_row(row: Any) -> Any:",
|
|
289
|
+
'\t"""Return a copy of a row with Python option names, such as label_colour, renamed to the camelCase names the renderer reads. Keys already in camelCase are kept, and a value that is not a dictionary is returned unchanged for the renderer to filter."""',
|
|
290
|
+
"\tif not isinstance(row, dict):",
|
|
291
|
+
"\t\treturn row",
|
|
292
|
+
"",
|
|
293
|
+
"\tconverted_row = {}",
|
|
294
|
+
"",
|
|
295
|
+
"\tfor key, value in row.items():",
|
|
296
|
+
'\t\tparts = key.split("_")',
|
|
297
|
+
'\t\tcamel_case_key = parts[0] + "".join(part.capitalize() for part in parts[1:])',
|
|
298
|
+
"\t\tconverted_row[camel_case_key] = value",
|
|
299
|
+
"",
|
|
300
|
+
"\treturn converted_row",
|
|
301
|
+
"",
|
|
302
|
+
"",
|
|
282
303
|
wrappers,
|
|
283
304
|
"",
|
|
284
305
|
].join("\n");
|
|
@@ -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;
|
|
@@ -595,7 +595,7 @@ const rendererDefinitions = [
|
|
|
595
595
|
default: "info",
|
|
596
596
|
},
|
|
597
597
|
{
|
|
598
|
-
name: "
|
|
598
|
+
name: "width",
|
|
599
599
|
type: "number",
|
|
600
600
|
required: false,
|
|
601
601
|
default: null,
|
|
@@ -635,6 +635,32 @@ const rendererDefinitions = [
|
|
|
635
635
|
],
|
|
636
636
|
fixture: undefined,
|
|
637
637
|
},
|
|
638
|
+
{
|
|
639
|
+
name: "quote-block",
|
|
640
|
+
category: "pattern",
|
|
641
|
+
api: "quoteBlock",
|
|
642
|
+
params: [
|
|
643
|
+
{
|
|
644
|
+
name: "lines",
|
|
645
|
+
type: "string[]",
|
|
646
|
+
required: false,
|
|
647
|
+
default: [],
|
|
648
|
+
},
|
|
649
|
+
{
|
|
650
|
+
name: "title",
|
|
651
|
+
type: "string",
|
|
652
|
+
required: false,
|
|
653
|
+
default: null,
|
|
654
|
+
},
|
|
655
|
+
{
|
|
656
|
+
name: "width",
|
|
657
|
+
type: "number",
|
|
658
|
+
required: false,
|
|
659
|
+
default: null,
|
|
660
|
+
},
|
|
661
|
+
],
|
|
662
|
+
fixture: "quote-block",
|
|
663
|
+
},
|
|
638
664
|
{
|
|
639
665
|
name: "row",
|
|
640
666
|
category: "primitive",
|
|
@@ -689,14 +715,12 @@ const rendererDefinitions = [
|
|
|
689
715
|
type: "boolean",
|
|
690
716
|
required: false,
|
|
691
717
|
default: true,
|
|
692
|
-
adapterOmit: true,
|
|
693
718
|
},
|
|
694
719
|
{
|
|
695
720
|
name: "wrapWidth",
|
|
696
721
|
type: "number",
|
|
697
722
|
required: false,
|
|
698
723
|
default: null,
|
|
699
|
-
adapterOmit: true,
|
|
700
724
|
},
|
|
701
725
|
],
|
|
702
726
|
fixture: undefined,
|
|
@@ -719,19 +743,35 @@ const rendererDefinitions = [
|
|
|
719
743
|
required: false,
|
|
720
744
|
default: null,
|
|
721
745
|
},
|
|
746
|
+
{
|
|
747
|
+
name: "labelColour",
|
|
748
|
+
type: "string",
|
|
749
|
+
required: false,
|
|
750
|
+
default: null,
|
|
751
|
+
},
|
|
752
|
+
{
|
|
753
|
+
name: "valueColour",
|
|
754
|
+
type: "string",
|
|
755
|
+
required: false,
|
|
756
|
+
default: null,
|
|
757
|
+
},
|
|
758
|
+
{
|
|
759
|
+
name: "separator",
|
|
760
|
+
type: "string",
|
|
761
|
+
required: false,
|
|
762
|
+
default: null,
|
|
763
|
+
},
|
|
722
764
|
{
|
|
723
765
|
name: "wrap",
|
|
724
766
|
type: "boolean",
|
|
725
767
|
required: false,
|
|
726
768
|
default: true,
|
|
727
|
-
adapterOmit: true,
|
|
728
769
|
},
|
|
729
770
|
{
|
|
730
771
|
name: "wrapWidth",
|
|
731
772
|
type: "number",
|
|
732
773
|
required: false,
|
|
733
774
|
default: null,
|
|
734
|
-
adapterOmit: true,
|
|
735
775
|
},
|
|
736
776
|
],
|
|
737
777
|
fixture: undefined,
|
|
@@ -6,6 +6,7 @@ import { confirmationResult } from "../patterns/confirmation-result.js";
|
|
|
6
6
|
import { diagnosticReport } from "../patterns/diagnostic-report.js";
|
|
7
7
|
import { diffBlock } from "../patterns/diff-block.js";
|
|
8
8
|
import { nextStepBlock } from "../patterns/next-step-block.js";
|
|
9
|
+
import { quoteBlock } from "../patterns/quote-block.js";
|
|
9
10
|
import { taskSummary } from "../patterns/task-summary.js";
|
|
10
11
|
import { barChart } from "../primitives/bar-chart.js";
|
|
11
12
|
import { bulletList } from "../primitives/bullet-list.js";
|
|
@@ -38,6 +39,7 @@ const renderers = {
|
|
|
38
39
|
bulletList(input.items, {
|
|
39
40
|
...input,
|
|
40
41
|
...options,
|
|
42
|
+
width: input.width ?? options.width,
|
|
41
43
|
}),
|
|
42
44
|
chip: (input, options) =>
|
|
43
45
|
chip(input.label, input.tone, {
|
|
@@ -63,6 +65,7 @@ const renderers = {
|
|
|
63
65
|
errorBlock(input.title, input.lines, {
|
|
64
66
|
...input,
|
|
65
67
|
...options,
|
|
68
|
+
width: input.width ?? options.width,
|
|
66
69
|
}),
|
|
67
70
|
hint: (input, options) =>
|
|
68
71
|
hint(input.message, {
|
|
@@ -73,12 +76,15 @@ const renderers = {
|
|
|
73
76
|
labelledLine(input.label, input.message, {
|
|
74
77
|
...input,
|
|
75
78
|
...options,
|
|
79
|
+
width: input.width ?? options.width,
|
|
76
80
|
}),
|
|
77
81
|
"next-step-block": (input, options) => nextStepBlock(input, options),
|
|
82
|
+
"quote-block": (input, options) => quoteBlock(input, options),
|
|
78
83
|
panel: (input, options) =>
|
|
79
84
|
panel({
|
|
80
85
|
...input,
|
|
81
86
|
...options,
|
|
87
|
+
width: input.width ?? options.width,
|
|
82
88
|
}),
|
|
83
89
|
"progress-bar": (input, options) =>
|
|
84
90
|
progressBar({
|
|
@@ -89,16 +95,19 @@ const renderers = {
|
|
|
89
95
|
row(input.label, input.value, {
|
|
90
96
|
...input,
|
|
91
97
|
...options,
|
|
98
|
+
width: input.width ?? options.width,
|
|
92
99
|
}),
|
|
93
100
|
"row-group": (input, options) =>
|
|
94
101
|
rowGroup({
|
|
95
102
|
...input,
|
|
96
103
|
...options,
|
|
104
|
+
width: input.width ?? options.width,
|
|
97
105
|
}),
|
|
98
106
|
sparkline: (input, options) =>
|
|
99
107
|
sparkline({
|
|
100
108
|
...input,
|
|
101
109
|
...options,
|
|
110
|
+
width: input.width ?? options.width,
|
|
102
111
|
}),
|
|
103
112
|
span: (input, options) =>
|
|
104
113
|
span(input.value, input.tone, {
|
|
@@ -124,6 +133,7 @@ const renderers = {
|
|
|
124
133
|
table({
|
|
125
134
|
...input,
|
|
126
135
|
...options,
|
|
136
|
+
width: input.width ?? options.width,
|
|
127
137
|
}),
|
|
128
138
|
"task-summary": (input, options) => taskSummary(input, options),
|
|
129
139
|
};
|
|
@@ -6,6 +6,7 @@ import { confirmationResult } from "../patterns/confirmation-result.js";
|
|
|
6
6
|
import { diagnosticReport } from "../patterns/diagnostic-report.js";
|
|
7
7
|
import { diffBlock } from "../patterns/diff-block.js";
|
|
8
8
|
import { nextStepBlock } from "../patterns/next-step-block.js";
|
|
9
|
+
import { quoteBlock } from "../patterns/quote-block.js";
|
|
9
10
|
import { taskSummary } from "../patterns/task-summary.js";
|
|
10
11
|
import { stripAnsi } from "../formatters/ansi.js";
|
|
11
12
|
import { renderSpinnerFrame } from "../interactive/spinner.js";
|
|
@@ -133,6 +134,7 @@ export const galleryFixtures = [
|
|
|
133
134
|
"confirmation-result",
|
|
134
135
|
"next-step-block",
|
|
135
136
|
"diff-block",
|
|
137
|
+
"quote-block",
|
|
136
138
|
"sparkline",
|
|
137
139
|
"spinner",
|
|
138
140
|
];
|
|
@@ -280,7 +282,6 @@ function galleryWidthOptions(width) {
|
|
|
280
282
|
return {
|
|
281
283
|
barWidth: Math.max(width - 18, 1),
|
|
282
284
|
dividerWidth: width,
|
|
283
|
-
panelWidth: width,
|
|
284
285
|
};
|
|
285
286
|
}
|
|
286
287
|
|
|
@@ -601,6 +602,19 @@ function renderPatterns(options, fixture) {
|
|
|
601
602
|
"diagnostic-report": ["Diagnostic report", report],
|
|
602
603
|
"diff-block": ["Diff block", diff],
|
|
603
604
|
"next-step-block": ["Next-step block", nextStep],
|
|
605
|
+
"quote-block": [
|
|
606
|
+
"Quote block",
|
|
607
|
+
quoteBlock(
|
|
608
|
+
{
|
|
609
|
+
lines: [
|
|
610
|
+
"A command can return a long explanation that wraps while the marker stays beside each continuation line.",
|
|
611
|
+
"A second line keeps the same marker.",
|
|
612
|
+
],
|
|
613
|
+
title: "Command output",
|
|
614
|
+
},
|
|
615
|
+
options,
|
|
616
|
+
),
|
|
617
|
+
],
|
|
604
618
|
reporter: ["Reporter", reporterOutput],
|
|
605
619
|
"task-summary": ["Task summary", summary],
|
|
606
620
|
};
|
|
@@ -636,6 +650,9 @@ function renderPatterns(options, fixture) {
|
|
|
636
650
|
"Diff block",
|
|
637
651
|
frameExample(diff, options),
|
|
638
652
|
"",
|
|
653
|
+
"Quote block",
|
|
654
|
+
frameExample(fixtures["quote-block"][1], options),
|
|
655
|
+
"",
|
|
639
656
|
"Compact data table",
|
|
640
657
|
frameExample(compactTable, options),
|
|
641
658
|
"",
|
|
@@ -790,7 +807,7 @@ function renderPrimitives(options) {
|
|
|
790
807
|
emptyState("No results", "tone: muted", options),
|
|
791
808
|
errorBlock("Failed", ["tone: danger"], {
|
|
792
809
|
...options,
|
|
793
|
-
|
|
810
|
+
width: 40,
|
|
794
811
|
}),
|
|
795
812
|
"",
|
|
796
813
|
"Steps",
|
package/src/index.js
CHANGED
|
@@ -38,6 +38,7 @@ export { confirmationResult } from "./patterns/confirmation-result.js";
|
|
|
38
38
|
export { diagnosticReport } from "./patterns/diagnostic-report.js";
|
|
39
39
|
export { diffBlock } from "./patterns/diff-block.js";
|
|
40
40
|
export { nextStepBlock } from "./patterns/next-step-block.js";
|
|
41
|
+
export { quoteBlock } from "./patterns/quote-block.js";
|
|
41
42
|
export { taskSummary } from "./patterns/task-summary.js";
|
|
42
43
|
export { barChart } from "./primitives/bar-chart.js";
|
|
43
44
|
export { bulletList } from "./primitives/bullet-list.js";
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { foreground, stripAnsi } from "../formatters/ansi.js";
|
|
2
|
+
import { wrapText } from "../formatters/wrap.js";
|
|
3
|
+
import { profiles } from "../profiles/profiles.js";
|
|
4
|
+
import { isNonEmptyString, isRecord, renderSection } from "./helpers.js";
|
|
5
|
+
|
|
6
|
+
// The total width, including the marker and its space, used when no width is given.
|
|
7
|
+
const defaultWidth = 66;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Render another program's output with a marker beside each line, under an optional heading.
|
|
11
|
+
*
|
|
12
|
+
* @param {object} quote
|
|
13
|
+
* Quoted output to show.
|
|
14
|
+
* @param {string[]} quote.lines
|
|
15
|
+
* Output lines, including any blank lines to preserve.
|
|
16
|
+
* @param {string} quote.title
|
|
17
|
+
* Optional heading above the output.
|
|
18
|
+
* @param {number} quote.width
|
|
19
|
+
* Optional total width, including the marker and its space.
|
|
20
|
+
* @param {object} options
|
|
21
|
+
* Rendering options, such as the profile, Unicode support, and a fallback width.
|
|
22
|
+
* @returns {object|string}
|
|
23
|
+
* The quote unchanged for the json profile. Otherwise the marked lines as one string,
|
|
24
|
+
* or an empty string when there are no lines to show.
|
|
25
|
+
*/
|
|
26
|
+
export function quoteBlock(quote, options = {}) {
|
|
27
|
+
if (options.profile === profiles.JSON) {
|
|
28
|
+
return quote;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (!isRecord(quote) || !Array.isArray(quote.lines)) {
|
|
32
|
+
return "";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Lines that are not strings are skipped so one bad entry does not hide the rest.
|
|
36
|
+
const lines = quote.lines.filter((line) => typeof line === "string");
|
|
37
|
+
|
|
38
|
+
if (lines.length === 0) {
|
|
39
|
+
return "";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// The width from the quote wins over the options, and an invalid width falls back to the default.
|
|
43
|
+
const requestedWidth = quote.width ?? options.width ?? defaultWidth;
|
|
44
|
+
|
|
45
|
+
const width =
|
|
46
|
+
Number.isInteger(requestedWidth) && requestedWidth > 0 ? requestedWidth : defaultWidth;
|
|
47
|
+
|
|
48
|
+
// The room left for text once the marker and its space are drawn.
|
|
49
|
+
const contentWidth = Math.max(width - 2, 1);
|
|
50
|
+
|
|
51
|
+
// Plain and CI output are written for logs and searches, so they get no colour and the ASCII marker.
|
|
52
|
+
const isPlainOutput = options.profile === profiles.CI || options.profile === profiles.PLAIN;
|
|
53
|
+
|
|
54
|
+
// The ASCII marker is also used whenever Unicode is off.
|
|
55
|
+
const marker = options.unicode === false || isPlainOutput ? "|" : "│";
|
|
56
|
+
|
|
57
|
+
// The marker is muted wherever colour is allowed.
|
|
58
|
+
const renderedMarker = isPlainOutput ? marker : foreground(marker, "muted", options);
|
|
59
|
+
|
|
60
|
+
// Each wrapped part gets its own marker, so continuation lines stay visibly quoted.
|
|
61
|
+
const renderedLines = lines.flatMap((line) =>
|
|
62
|
+
wrapText(stripAnsi(line), contentWidth).map((part) => `${renderedMarker} ${part}`),
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
if (!isNonEmptyString(quote.title)) {
|
|
66
|
+
return renderedLines.join("\n");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return renderSection(stripAnsi(quote.title), renderedLines, options, false);
|
|
70
|
+
}
|
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
|
/**
|