@lewishowles/cli-style 0.12.1 → 0.14.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 +23 -0
- package/README.md +66 -10
- package/adapters/python/cli_style/__init__.py +73 -0
- package/adapters/python/cli_style/_wrappers.py +503 -0
- package/adapters/python/cli_style/core.py +136 -0
- package/adapters/python/pyproject.toml +15 -0
- package/adapters/swift/Package.swift +16 -0
- package/adapters/swift/Sources/CliStyle/CliStyle.swift +223 -0
- package/adapters/swift/Sources/CliStyle/Wrappers.swift +582 -0
- package/package.json +4 -2
- package/scripts/build-binary.sh +15 -3
- package/scripts/generate-adapters.js +413 -0
- package/scripts/package-binary.sh +4 -1
- package/src/catalogue/renderer-catalogue.js +958 -52
- package/src/cli/adapter-path-command.js +3 -3
- package/src/cli/render-command.js +6 -0
- package/src/gallery/render-gallery.js +80 -0
- package/src/index.js +3 -0
- package/src/patterns/audit-finding.js +4 -4
- package/src/patterns/command-result.js +2 -2
- package/src/patterns/diagnostic-report.js +6 -3
- package/src/patterns/helpers.js +11 -4
- package/src/patterns/next-step-block.js +2 -4
- package/src/patterns/task-summary.js +4 -4
- package/src/primitives/bullet-list.js +150 -0
- package/src/primitives/table.js +103 -22
- package/src/theme/bullets.js +41 -0
- package/adapters/python/cli_style.py +0 -384
- package/adapters/swift/CliStyle.swift +0 -482
|
@@ -2,7 +2,7 @@ import { existsSync } from "node:fs";
|
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
|
|
5
|
-
// Adapter paths returned for
|
|
5
|
+
// Adapter paths returned for consumer setup.
|
|
6
6
|
const adapterPaths = {
|
|
7
7
|
bash: {
|
|
8
8
|
releasePath: "adapters/bash/cli-style.sh",
|
|
@@ -13,8 +13,8 @@ const adapterPaths = {
|
|
|
13
13
|
sourceUrl: new URL("../../adapters/python", import.meta.url),
|
|
14
14
|
},
|
|
15
15
|
swift: {
|
|
16
|
-
releasePath: "adapters/swift
|
|
17
|
-
sourceUrl: new URL("../../adapters/swift
|
|
16
|
+
releasePath: "adapters/swift",
|
|
17
|
+
sourceUrl: new URL("../../adapters/swift", import.meta.url),
|
|
18
18
|
},
|
|
19
19
|
};
|
|
20
20
|
|
|
@@ -8,6 +8,7 @@ import { diffBlock } from "../patterns/diff-block.js";
|
|
|
8
8
|
import { nextStepBlock } from "../patterns/next-step-block.js";
|
|
9
9
|
import { taskSummary } from "../patterns/task-summary.js";
|
|
10
10
|
import { barChart } from "../primitives/bar-chart.js";
|
|
11
|
+
import { bulletList } from "../primitives/bullet-list.js";
|
|
11
12
|
import { chip } from "../primitives/chip.js";
|
|
12
13
|
import { divider } from "../primitives/divider.js";
|
|
13
14
|
import { emptyState, errorBlock, hint } from "../primitives/feedback.js";
|
|
@@ -33,6 +34,11 @@ const renderers = {
|
|
|
33
34
|
...input,
|
|
34
35
|
...options,
|
|
35
36
|
}),
|
|
37
|
+
"bullet-list": (input, options) =>
|
|
38
|
+
bulletList(input.items, {
|
|
39
|
+
...input,
|
|
40
|
+
...options,
|
|
41
|
+
}),
|
|
36
42
|
chip: (input, options) =>
|
|
37
43
|
chip(input.label, input.tone, {
|
|
38
44
|
...input,
|
|
@@ -10,6 +10,7 @@ import { taskSummary } from "../patterns/task-summary.js";
|
|
|
10
10
|
import { stripAnsi } from "../formatters/ansi.js";
|
|
11
11
|
import { renderSpinnerFrame } from "../interactive/spinner.js";
|
|
12
12
|
import { barChart } from "../primitives/bar-chart.js";
|
|
13
|
+
import { bulletList } from "../primitives/bullet-list.js";
|
|
13
14
|
import { chip } from "../primitives/chip.js";
|
|
14
15
|
import { divider } from "../primitives/divider.js";
|
|
15
16
|
import { emptyState, errorBlock, hint } from "../primitives/feedback.js";
|
|
@@ -764,12 +765,21 @@ function renderPrimitives(options) {
|
|
|
764
765
|
"Tables",
|
|
765
766
|
renderTableExample(options),
|
|
766
767
|
"",
|
|
768
|
+
"Truncated tables",
|
|
769
|
+
renderOverflowTableExample(options, "truncate"),
|
|
770
|
+
"",
|
|
771
|
+
"Wrapped tables",
|
|
772
|
+
renderOverflowTableExample(options, "wrap"),
|
|
773
|
+
"",
|
|
767
774
|
"Narrow tables",
|
|
768
775
|
renderTableExample({
|
|
769
776
|
...options,
|
|
770
777
|
width: 18,
|
|
771
778
|
}),
|
|
772
779
|
"",
|
|
780
|
+
"Bullet lists",
|
|
781
|
+
renderBulletListExamples(options),
|
|
782
|
+
"",
|
|
773
783
|
"Feedback",
|
|
774
784
|
hint(`Run ${span("npm run docs:readme", "info", options)}`, options),
|
|
775
785
|
labelledLine("Notice", "Reusable labelled output", {
|
|
@@ -866,6 +876,42 @@ function renderPrimitives(options) {
|
|
|
866
876
|
].join("\n");
|
|
867
877
|
}
|
|
868
878
|
|
|
879
|
+
/**
|
|
880
|
+
* Render representative flat, wrapped, nested, narrow, and plain bullet lists.
|
|
881
|
+
*
|
|
882
|
+
* @param {object} options
|
|
883
|
+
* Rendering options.
|
|
884
|
+
* @returns {string}
|
|
885
|
+
* Bullet list examples.
|
|
886
|
+
*/
|
|
887
|
+
function renderBulletListExamples(options) {
|
|
888
|
+
return [
|
|
889
|
+
bulletList(["Install dependencies", "Run the unit tests", "Review the output"], options),
|
|
890
|
+
bulletList(["This item wraps so continuation lines stay aligned with the item text."], {
|
|
891
|
+
...options,
|
|
892
|
+
width: 40,
|
|
893
|
+
}),
|
|
894
|
+
bulletList(
|
|
895
|
+
[
|
|
896
|
+
{
|
|
897
|
+
items: ["Use the focused check", "Read the first failure"],
|
|
898
|
+
text: "Verification",
|
|
899
|
+
},
|
|
900
|
+
"Commit the completed chunk",
|
|
901
|
+
],
|
|
902
|
+
options,
|
|
903
|
+
),
|
|
904
|
+
bulletList(["Narrow output keeps the gutter readable."], {
|
|
905
|
+
...options,
|
|
906
|
+
width: 24,
|
|
907
|
+
}),
|
|
908
|
+
bulletList(["ASCII output remains easy to grep."], {
|
|
909
|
+
...options,
|
|
910
|
+
profile: "plain",
|
|
911
|
+
}),
|
|
912
|
+
].join("\n");
|
|
913
|
+
}
|
|
914
|
+
|
|
869
915
|
/**
|
|
870
916
|
* Render wide or narrow table diagnostics.
|
|
871
917
|
*
|
|
@@ -900,6 +946,40 @@ function renderTableExample(options) {
|
|
|
900
946
|
});
|
|
901
947
|
}
|
|
902
948
|
|
|
949
|
+
/**
|
|
950
|
+
* Render a table with a long-detail column capped by maxWidth.
|
|
951
|
+
*
|
|
952
|
+
* @param {object} options
|
|
953
|
+
* Rendering options.
|
|
954
|
+
* @param {string} overflow
|
|
955
|
+
* Overflow mode ('truncate' or 'wrap') for the capped column.
|
|
956
|
+
* @returns {string}
|
|
957
|
+
* Table example.
|
|
958
|
+
*/
|
|
959
|
+
function renderOverflowTableExample(options, overflow) {
|
|
960
|
+
return table({
|
|
961
|
+
...options,
|
|
962
|
+
columns: [
|
|
963
|
+
{
|
|
964
|
+
key: "source",
|
|
965
|
+
label: "Source",
|
|
966
|
+
},
|
|
967
|
+
{
|
|
968
|
+
key: "detail",
|
|
969
|
+
label: "Detail",
|
|
970
|
+
maxWidth: 28,
|
|
971
|
+
overflow,
|
|
972
|
+
},
|
|
973
|
+
],
|
|
974
|
+
rows: [
|
|
975
|
+
{
|
|
976
|
+
detail: "packages/cli-style/src/primitives/table.js",
|
|
977
|
+
source: "Table",
|
|
978
|
+
},
|
|
979
|
+
],
|
|
980
|
+
});
|
|
981
|
+
}
|
|
982
|
+
|
|
903
983
|
/**
|
|
904
984
|
* Render a chart that exposes positive, warning, and negative colours.
|
|
905
985
|
*
|
package/src/index.js
CHANGED
|
@@ -40,6 +40,7 @@ export { diffBlock } from "./patterns/diff-block.js";
|
|
|
40
40
|
export { nextStepBlock } from "./patterns/next-step-block.js";
|
|
41
41
|
export { taskSummary } from "./patterns/task-summary.js";
|
|
42
42
|
export { barChart } from "./primitives/bar-chart.js";
|
|
43
|
+
export { bulletList } from "./primitives/bullet-list.js";
|
|
43
44
|
export { chip } from "./primitives/chip.js";
|
|
44
45
|
export { divider } from "./primitives/divider.js";
|
|
45
46
|
export { emptyState, errorBlock, hint } from "./primitives/feedback.js";
|
|
@@ -62,6 +63,8 @@ export {
|
|
|
62
63
|
renderSection,
|
|
63
64
|
} from "./reporters/create-reporter.js";
|
|
64
65
|
|
|
66
|
+
export { bulletTokens, getBulletSymbol } from "./theme/bullets.js";
|
|
67
|
+
|
|
65
68
|
export {
|
|
66
69
|
chartColours,
|
|
67
70
|
chipColours,
|
|
@@ -48,15 +48,15 @@ export function auditFinding(finding, options = {}) {
|
|
|
48
48
|
|
|
49
49
|
const title = isNonEmptyString(finding.title) ? finding.title : defaultTitle;
|
|
50
50
|
const summary = isNonEmptyString(finding.finding) ? finding.finding : "";
|
|
51
|
-
const evidence = normaliseStringList(finding.evidence)
|
|
52
|
-
const references = normaliseStringList(finding.references)
|
|
51
|
+
const evidence = normaliseStringList(finding.evidence);
|
|
52
|
+
const references = normaliseStringList(finding.references);
|
|
53
53
|
|
|
54
54
|
const sections = [
|
|
55
55
|
status(finding.result ?? resultTypes.UNKNOWN, summary, options),
|
|
56
56
|
renderLocation(finding.location, options),
|
|
57
|
-
renderSection("Evidence", evidence, options,
|
|
57
|
+
renderSection("Evidence", evidence, options, true),
|
|
58
58
|
renderTextSection("Recommendation", finding.recommendation, options),
|
|
59
|
-
renderSection("References", references, options,
|
|
59
|
+
renderSection("References", references, options, true),
|
|
60
60
|
].filter((section) => section !== "");
|
|
61
61
|
|
|
62
62
|
return [renderTitle(title, options), ...sections].join("\n\n");
|
|
@@ -48,13 +48,13 @@ export function commandResult(result, options = {}) {
|
|
|
48
48
|
|
|
49
49
|
const title = isNonEmptyString(result.title) ? result.title : defaultTitle;
|
|
50
50
|
const metadata = renderMetadata(result, options);
|
|
51
|
-
const details = normaliseStringList(result.details)
|
|
51
|
+
const details = normaliseStringList(result.details);
|
|
52
52
|
const summary = isNonEmptyString(result.summary) ? result.summary : "";
|
|
53
53
|
|
|
54
54
|
const sections = [
|
|
55
55
|
status(result.result ?? resultTypes.UNKNOWN, summary, options),
|
|
56
56
|
metadata,
|
|
57
|
-
renderSection("Details", details, options,
|
|
57
|
+
renderSection("Details", details, options, true),
|
|
58
58
|
].filter((section) => section !== "");
|
|
59
59
|
|
|
60
60
|
return [renderTitle(title, options), ...sections].join("\n\n");
|
|
@@ -69,7 +69,8 @@ function renderChecks(checks, options) {
|
|
|
69
69
|
status(check.result, formatLabel(check.name, check.detail, options), options),
|
|
70
70
|
);
|
|
71
71
|
|
|
72
|
-
|
|
72
|
+
// Status lines already start with a symbol, so only agent Markdown adds bullets.
|
|
73
|
+
return renderSection("Checks", lines, options, options.profile === profiles.AGENT);
|
|
73
74
|
}
|
|
74
75
|
|
|
75
76
|
/**
|
|
@@ -89,7 +90,8 @@ function renderFindings(findings, options) {
|
|
|
89
90
|
|
|
90
91
|
const lines = validFindings.map((finding) => status(finding.result, finding.message, options));
|
|
91
92
|
|
|
92
|
-
|
|
93
|
+
// Only agent Markdown adds bullets beside the status symbol.
|
|
94
|
+
return renderSection("Findings", lines, options, options.profile === profiles.AGENT);
|
|
93
95
|
}
|
|
94
96
|
|
|
95
97
|
/**
|
|
@@ -111,7 +113,8 @@ function renderSkippedChecks(skippedChecks, options) {
|
|
|
111
113
|
status(resultTypes.SKIPPED, formatLabel(check.name, check.reason, options), options),
|
|
112
114
|
);
|
|
113
115
|
|
|
114
|
-
|
|
116
|
+
// Only agent Markdown adds bullets beside the status symbol.
|
|
117
|
+
return renderSection("Skipped checks", lines, options, options.profile === profiles.AGENT);
|
|
115
118
|
}
|
|
116
119
|
|
|
117
120
|
/**
|
package/src/patterns/helpers.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { bulletList } from "../primitives/bullet-list.js";
|
|
1
2
|
import { profiles } from "../profiles/profiles.js";
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -72,7 +73,8 @@ export function normaliseStringList(values) {
|
|
|
72
73
|
* @param {object} options
|
|
73
74
|
* Rendering options.
|
|
74
75
|
* @param {boolean} listItems
|
|
75
|
-
* Whether
|
|
76
|
+
* Whether to show each line as a bullet point, wrapped to the terminal
|
|
77
|
+
* width. Pass false for single values, commands, or numbered lines.
|
|
76
78
|
* @returns {string}
|
|
77
79
|
* Rendered section.
|
|
78
80
|
*/
|
|
@@ -83,10 +85,15 @@ export function renderSection(title, lines, options, listItems = true) {
|
|
|
83
85
|
|
|
84
86
|
const heading = options.profile === profiles.AGENT ? `## ${title}` : title;
|
|
85
87
|
|
|
86
|
-
const content =
|
|
87
|
-
|
|
88
|
+
const content = listItems
|
|
89
|
+
? bulletList(lines, {
|
|
90
|
+
profile: options.profile,
|
|
91
|
+
unicode: options.unicode,
|
|
92
|
+
width: options.width,
|
|
93
|
+
})
|
|
94
|
+
: lines.join("\n");
|
|
88
95
|
|
|
89
|
-
return [heading,
|
|
96
|
+
return [heading, content].join("\n");
|
|
90
97
|
}
|
|
91
98
|
|
|
92
99
|
/**
|
|
@@ -49,15 +49,13 @@ export function nextStepBlock(nextStep, options = {}) {
|
|
|
49
49
|
|
|
50
50
|
const commands = renderCommands(nextStep.commands, options);
|
|
51
51
|
|
|
52
|
-
const alternatives = normaliseStringList(nextStep.alternatives)
|
|
53
|
-
(alternative) => `- ${alternative}`,
|
|
54
|
-
);
|
|
52
|
+
const alternatives = normaliseStringList(nextStep.alternatives);
|
|
55
53
|
|
|
56
54
|
const sections = [
|
|
57
55
|
next,
|
|
58
56
|
reason,
|
|
59
57
|
commands,
|
|
60
|
-
renderSection("Alternatives", alternatives, options,
|
|
58
|
+
renderSection("Alternatives", alternatives, options, true),
|
|
61
59
|
].filter((section) => section !== "");
|
|
62
60
|
|
|
63
61
|
return [renderTitle(title, options), ...sections].join("\n\n");
|
|
@@ -47,13 +47,13 @@ export function taskSummary(summary, options = {}) {
|
|
|
47
47
|
const title = isNonEmptyString(summary.title) ? summary.title : defaultTitle;
|
|
48
48
|
const task = isNonEmptyString(summary.task) ? summary.task : "";
|
|
49
49
|
const detail = formatLabel(task, summary.summary, options);
|
|
50
|
-
const completed = normaliseStringList(summary.completed)
|
|
51
|
-
const remaining = normaliseStringList(summary.remaining)
|
|
50
|
+
const completed = normaliseStringList(summary.completed);
|
|
51
|
+
const remaining = normaliseStringList(summary.remaining);
|
|
52
52
|
|
|
53
53
|
const sections = [
|
|
54
54
|
status(summary.result ?? resultTypes.UNKNOWN, detail, options),
|
|
55
|
-
renderSection("Completed", completed, options,
|
|
56
|
-
renderSection("Remaining", remaining, options,
|
|
55
|
+
renderSection("Completed", completed, options, true),
|
|
56
|
+
renderSection("Remaining", remaining, options, true),
|
|
57
57
|
].filter((section) => section !== "");
|
|
58
58
|
|
|
59
59
|
return [renderTitle(title, options), ...sections].join("\n\n");
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { wrapText } from "../formatters/wrap.js";
|
|
2
|
+
import { getBulletSymbol } from "../theme/bullets.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Render items as a bullet list, wrapping each item's text beside its bullet.
|
|
6
|
+
*
|
|
7
|
+
* @param {object[]} items
|
|
8
|
+
* Item text, or `{ text, items }` for an item with nested items. Empty and
|
|
9
|
+
* malformed items are skipped.
|
|
10
|
+
* @param {object} options
|
|
11
|
+
* Rendering and wrapping options.
|
|
12
|
+
* @param {number} options.width
|
|
13
|
+
* Total line width used to calculate the available item width.
|
|
14
|
+
* @param {number} options.wrapWidth
|
|
15
|
+
* Explicit text width for each item.
|
|
16
|
+
* @returns {string}
|
|
17
|
+
* Rendered bullet list, or an empty string when no usable items exist.
|
|
18
|
+
*/
|
|
19
|
+
export function bulletList(items = [], options = {}) {
|
|
20
|
+
const normalisedItems = normaliseItems(items);
|
|
21
|
+
|
|
22
|
+
if (normalisedItems.length === 0) {
|
|
23
|
+
return "";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return renderItems(normalisedItems, options, 0).join("\n");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Render one level of items and its nested children.
|
|
31
|
+
*
|
|
32
|
+
* @param {object[]} items
|
|
33
|
+
* Normalised entries at the current nesting level.
|
|
34
|
+
* @param {object} options
|
|
35
|
+
* Rendering and wrapping options.
|
|
36
|
+
* @param {number} depth
|
|
37
|
+
* Zero-based nesting depth.
|
|
38
|
+
* @returns {string[]}
|
|
39
|
+
* Rendered lines for this level and its children.
|
|
40
|
+
*/
|
|
41
|
+
function renderItems(items, options, depth) {
|
|
42
|
+
const lines = [];
|
|
43
|
+
|
|
44
|
+
for (const entry of items) {
|
|
45
|
+
const prefix = createPrefix(depth, options);
|
|
46
|
+
const itemLines = wrapText(entry.text, resolveWrapWidth(prefix, options));
|
|
47
|
+
|
|
48
|
+
lines.push(
|
|
49
|
+
...itemLines.map((line, index) => {
|
|
50
|
+
if (index === 0) {
|
|
51
|
+
return `${prefix}${line}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return `${" ".repeat(prefix.length)}${line}`;
|
|
55
|
+
}),
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
const childItems = normaliseItems(entry.items);
|
|
59
|
+
|
|
60
|
+
if (childItems.length > 0) {
|
|
61
|
+
lines.push(...renderItems(childItems, options, depth + 1));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return lines;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Build the plain-text prefix used for one item and its continuations.
|
|
70
|
+
*
|
|
71
|
+
* @param {number} depth
|
|
72
|
+
* Zero-based nesting depth.
|
|
73
|
+
* @param {object} options
|
|
74
|
+
* Symbol rendering options.
|
|
75
|
+
* @returns {string}
|
|
76
|
+
* Uncoloured indentation, bullet, and trailing space.
|
|
77
|
+
*/
|
|
78
|
+
function createPrefix(depth, options) {
|
|
79
|
+
const symbol = getBulletSymbol(depth, options);
|
|
80
|
+
const gutter = `${symbol} `;
|
|
81
|
+
const indentation = " ".repeat(depth * gutter.length);
|
|
82
|
+
|
|
83
|
+
return `${indentation}${gutter}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Resolve the text width available after the item prefix.
|
|
88
|
+
*
|
|
89
|
+
* @param {string} prefix
|
|
90
|
+
* Plain-text item prefix.
|
|
91
|
+
* @param {object} options
|
|
92
|
+
* Rendering and wrapping options.
|
|
93
|
+
* @returns {number|undefined}
|
|
94
|
+
* Explicit text width, derived text width, or undefined for wrapText's default.
|
|
95
|
+
*/
|
|
96
|
+
function resolveWrapWidth(prefix, options) {
|
|
97
|
+
if (options.wrapWidth !== undefined) {
|
|
98
|
+
return options.wrapWidth;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (Number.isFinite(options.width)) {
|
|
102
|
+
return Math.max(1, options.width - prefix.length);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Convert a list value into renderable entries, dropping unusable items.
|
|
110
|
+
*
|
|
111
|
+
* @param {*} items
|
|
112
|
+
* Requested list entries.
|
|
113
|
+
* @returns {object[]}
|
|
114
|
+
* Non-empty strings and valid nested entries.
|
|
115
|
+
*/
|
|
116
|
+
function normaliseItems(items) {
|
|
117
|
+
if (!Array.isArray(items)) {
|
|
118
|
+
return [];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return items.map(normaliseItem).filter((entry) => entry !== null);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Convert one supported input value into a list entry.
|
|
126
|
+
*
|
|
127
|
+
* @param {*} item
|
|
128
|
+
* Requested list value.
|
|
129
|
+
* @returns {object|null}
|
|
130
|
+
* Original object or a new entry for a string, or null when the value has no usable text.
|
|
131
|
+
*/
|
|
132
|
+
function normaliseItem(item) {
|
|
133
|
+
if (typeof item === "string" && item.trim() !== "") {
|
|
134
|
+
return {
|
|
135
|
+
items: [],
|
|
136
|
+
text: item,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (
|
|
141
|
+
item !== null &&
|
|
142
|
+
typeof item === "object" &&
|
|
143
|
+
typeof item.text === "string" &&
|
|
144
|
+
item.text.trim() !== ""
|
|
145
|
+
) {
|
|
146
|
+
return item;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return null;
|
|
150
|
+
}
|
package/src/primitives/table.js
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import { foreground } from "../formatters/ansi.js";
|
|
2
|
+
import { defaultWidth, minimumWidth, normaliseWidth } from "../formatters/width.js";
|
|
2
3
|
import { tableColours } from "../theme/colours.js";
|
|
3
4
|
|
|
4
|
-
// Tables assume a standard terminal width when no capability is provided.
|
|
5
|
-
const defaultWidth = 80;
|
|
6
|
-
|
|
7
5
|
// Columns use a compact but readable gap.
|
|
8
6
|
const columnSeparator = " ";
|
|
9
7
|
|
|
@@ -15,7 +13,9 @@ const columnSeparator = " ";
|
|
|
15
13
|
* @param {boolean} options.colour
|
|
16
14
|
* Whether ANSI colour should be applied.
|
|
17
15
|
* @param {object[]} options.columns
|
|
18
|
-
* Column keys and labels.
|
|
16
|
+
* Column keys and labels. A column may set maxWidth to cap its width,
|
|
17
|
+
* with overflow ('truncate', the default, or 'wrap') controlling how
|
|
18
|
+
* values beyond that width render.
|
|
19
19
|
* @param {object[]} options.rows
|
|
20
20
|
* Table records.
|
|
21
21
|
* @param {boolean} options.unicode
|
|
@@ -39,7 +39,18 @@ export function table(options = {}) {
|
|
|
39
39
|
widths.reduce((total, width) => total + width, 0) +
|
|
40
40
|
columnSeparator.length * (columns.length - 1);
|
|
41
41
|
|
|
42
|
-
const
|
|
42
|
+
const requestedWidth = Number.isFinite(options.width) ? options.width : undefined;
|
|
43
|
+
|
|
44
|
+
// Let a requested width smaller than minimumWidth pass through unclamped.
|
|
45
|
+
const minimumTableWidth =
|
|
46
|
+
requestedWidth === undefined
|
|
47
|
+
? minimumWidth
|
|
48
|
+
: Math.max(1, Math.min(requestedWidth, minimumWidth));
|
|
49
|
+
|
|
50
|
+
const availableWidth = normaliseWidth(requestedWidth, {
|
|
51
|
+
defaultWidth,
|
|
52
|
+
minWidth: minimumTableWidth,
|
|
53
|
+
});
|
|
43
54
|
|
|
44
55
|
if (naturalWidth > availableWidth) {
|
|
45
56
|
return renderBlocks(columns, rows, options);
|
|
@@ -99,9 +110,65 @@ function normaliseRows(rows) {
|
|
|
99
110
|
* Column widths.
|
|
100
111
|
*/
|
|
101
112
|
function columnWidths(columns, rows) {
|
|
102
|
-
return columns.map((column) =>
|
|
103
|
-
Math.max(
|
|
104
|
-
|
|
113
|
+
return columns.map((column) => {
|
|
114
|
+
const naturalWidth = Math.max(
|
|
115
|
+
column.label.length,
|
|
116
|
+
...rows.map((row) => cellValue(row[column.key]).length),
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
const maxWidth = columnMaxWidth(column);
|
|
120
|
+
|
|
121
|
+
return maxWidth === null ? naturalWidth : Math.min(naturalWidth, maxWidth);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Return a column's configured width cap, if any.
|
|
127
|
+
*
|
|
128
|
+
* @param {object} column
|
|
129
|
+
* Table column.
|
|
130
|
+
* @returns {number|null}
|
|
131
|
+
* Normalised maxWidth, or null when the column has none.
|
|
132
|
+
*/
|
|
133
|
+
function columnMaxWidth(column) {
|
|
134
|
+
if (!Number.isFinite(column.maxWidth) || column.maxWidth < 1) {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return normaliseWidth(column.maxWidth, {
|
|
139
|
+
defaultWidth: 1,
|
|
140
|
+
minWidth: 1,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Split a cell's value into the lines it renders as within a fixed-width column.
|
|
146
|
+
*
|
|
147
|
+
* @param {string} value
|
|
148
|
+
* Cell value.
|
|
149
|
+
* @param {object} column
|
|
150
|
+
* Table column, read for its overflow mode.
|
|
151
|
+
* @param {number} width
|
|
152
|
+
* Column width.
|
|
153
|
+
* @returns {string[]}
|
|
154
|
+
* Lines the cell renders as; truncated to one line with a trailing
|
|
155
|
+
* ellipsis unless the column's overflow is 'wrap'.
|
|
156
|
+
*/
|
|
157
|
+
function columnLines(value, column, width) {
|
|
158
|
+
if (value.length <= width) {
|
|
159
|
+
return [value];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (column.overflow === "wrap") {
|
|
163
|
+
return Array.from(
|
|
164
|
+
{
|
|
165
|
+
length: Math.ceil(value.length / width),
|
|
166
|
+
},
|
|
167
|
+
(_, index) => value.slice(index * width, (index + 1) * width),
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return [`${value.slice(0, width - 1)}…`];
|
|
105
172
|
}
|
|
106
173
|
|
|
107
174
|
/**
|
|
@@ -120,9 +187,11 @@ function columnWidths(columns, rows) {
|
|
|
120
187
|
*/
|
|
121
188
|
function renderTable(columns, rows, widths, options) {
|
|
122
189
|
const header = columns
|
|
123
|
-
.map((column, index) =>
|
|
124
|
-
|
|
125
|
-
|
|
190
|
+
.map((column, index) => {
|
|
191
|
+
const headerLine = columnLines(column.label, { overflow: "truncate" }, widths[index])[0];
|
|
192
|
+
|
|
193
|
+
return renderHeader(padColumn(headerLine, widths[index], index, columns.length), options);
|
|
194
|
+
})
|
|
126
195
|
.join(columnSeparator);
|
|
127
196
|
|
|
128
197
|
const ruleCharacter = options.unicode === false ? "-" : "─";
|
|
@@ -131,17 +200,29 @@ function renderTable(columns, rows, widths, options) {
|
|
|
131
200
|
const renderedRule =
|
|
132
201
|
options.colour === true ? foreground(rule, tableColours.border, options) : rule;
|
|
133
202
|
|
|
134
|
-
const body = rows.
|
|
135
|
-
columns
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
203
|
+
const body = rows.flatMap((row) => {
|
|
204
|
+
const lines = columns.map((column, index) =>
|
|
205
|
+
columnLines(cellValue(row[column.key]), column, widths[index]),
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
const lineCount = Math.max(...lines.map((column) => column.length));
|
|
209
|
+
|
|
210
|
+
return Array.from(
|
|
211
|
+
{
|
|
212
|
+
length: lineCount,
|
|
213
|
+
},
|
|
214
|
+
(_, lineIndex) =>
|
|
215
|
+
columns
|
|
216
|
+
.map((column, index) =>
|
|
217
|
+
renderCell(
|
|
218
|
+
padColumn(lines[index][lineIndex] ?? "", widths[index], index, columns.length),
|
|
219
|
+
index,
|
|
220
|
+
options,
|
|
221
|
+
),
|
|
222
|
+
)
|
|
223
|
+
.join(columnSeparator),
|
|
224
|
+
);
|
|
225
|
+
});
|
|
145
226
|
|
|
146
227
|
return [header, renderedRule, ...body].join("\n");
|
|
147
228
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { profiles } from "../profiles/profiles.js";
|
|
2
|
+
|
|
3
|
+
// Bullet symbols for Unicode, ASCII, and Markdown-compatible agent output.
|
|
4
|
+
export const bulletTokens = {
|
|
5
|
+
agent: "-",
|
|
6
|
+
ascii: "*",
|
|
7
|
+
unicode: {
|
|
8
|
+
nested: "◦",
|
|
9
|
+
root: "•",
|
|
10
|
+
},
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Resolve the display symbol for one bullet depth.
|
|
15
|
+
*
|
|
16
|
+
* @param {number} depth
|
|
17
|
+
* Zero-based nesting depth.
|
|
18
|
+
* @param {object} options
|
|
19
|
+
* Symbol rendering options.
|
|
20
|
+
* @param {string} options.profile
|
|
21
|
+
* Active output profile.
|
|
22
|
+
* @param {boolean} options.unicode
|
|
23
|
+
* Whether to return the Unicode symbol.
|
|
24
|
+
* @returns {string}
|
|
25
|
+
* Display symbol for the requested bullet depth and profile.
|
|
26
|
+
*/
|
|
27
|
+
export function getBulletSymbol(depth = 0, options = {}) {
|
|
28
|
+
if (options.profile === profiles.AGENT) {
|
|
29
|
+
return bulletTokens.agent;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (
|
|
33
|
+
options.profile === profiles.CI ||
|
|
34
|
+
options.profile === profiles.PLAIN ||
|
|
35
|
+
options.unicode === false
|
|
36
|
+
) {
|
|
37
|
+
return bulletTokens.ascii;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return depth === 0 ? bulletTokens.unicode.root : bulletTokens.unicode.nested;
|
|
41
|
+
}
|