@flowscripter/dynamic-cli-framework 1.1.6 → 1.2.1
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/.github/workflows/release-bun-library.yml +1 -0
- package/README/core-commands.md +40 -0
- package/README/core-service-providers.md +198 -0
- package/README/development.md +60 -0
- package/README/implementation-details.md +474 -0
- package/README/key-concepts.md +751 -0
- package/README.md +12 -1413
- package/bun.lock +22 -18
- package/index.ts +97 -1
- package/package.json +8 -7
- package/src/api/BaseCLIFeatureOptions.ts +10 -0
- package/src/api/CLIConfig.ts +5 -0
- package/src/api/RunResult.ts +16 -0
- package/src/api/argument/Argument.ts +24 -0
- package/src/api/service/ServiceProvider.ts +3 -3
- package/src/api/service/core/ArgumentPrompterService.ts +10 -0
- package/src/api/service/core/AsciiBannerGeneratorService.ts +44 -7
- package/src/api/service/core/CompletionService.ts +36 -0
- package/src/api/service/core/DataDumpGeneratorService.ts +27 -0
- package/src/api/service/core/ImagePrinterService.ts +26 -0
- package/src/api/service/core/KeyValueService.ts +22 -5
- package/src/api/service/core/PrinterService.ts +127 -1
- package/src/api/service/core/PrompterService.ts +49 -0
- package/src/api/service/core/SecretService.ts +27 -0
- package/src/api/service/core/ShutdownService.ts +17 -1
- package/src/api/service/core/Table.ts +85 -0
- package/src/api/service/core/TableGeneratorService.ts +38 -0
- package/src/api/service/core/TreePrinterService.ts +11 -0
- package/src/cli/BaseCLI.ts +116 -44
- package/src/cli/DefaultRuntimeCLI.ts +10 -18
- package/src/command/CompletionCommand.ts +179 -0
- package/src/command/MultiCommandCliHelpCommand.ts +18 -2
- package/src/command/SingleCommandCliHelpCommand.ts +10 -1
- package/src/launcher.ts +16 -58
- package/src/runtime/command/CommandValidator.ts +3 -3
- package/src/runtime/runner.ts +102 -30
- package/src/runtime/scanner.ts +18 -15
- package/src/runtime/values/argumentValueValidation.ts +49 -5
- package/src/runtime/values/globalCommandValuePopulation.ts +1 -1
- package/src/runtime/values/subCommandValuePopulation.ts +9 -8
- package/src/service/argumentPrompter/ArgumentPrompterServiceProvider.ts +34 -0
- package/src/service/argumentPrompter/DefaultArgumentPrompterService.ts +330 -0
- package/src/service/asciiBannerGenerator/AsciiBannerGeneratorServiceProvider.ts +1 -1
- package/src/service/asciiBannerGenerator/DefaultAsciiBannerGeneratorService.ts +254 -17
- package/src/service/banner/BannerServiceProvider.ts +2 -2
- package/src/service/chiselAsciiBannerGenerator/ChiselFontAsciiBannerGeneratorService.ts +85 -0
- package/src/service/chiselAsciiBannerGenerator/chisel.flf.json +3 -0
- package/src/service/completion/CompletionServiceProvider.ts +212 -0
- package/src/service/completion/DefaultCompletionService.ts +364 -0
- package/src/service/completion/shell/ShellHandler.ts +16 -0
- package/src/service/completion/shell/bash.ts +47 -0
- package/src/service/completion/shell/fish.ts +43 -0
- package/src/service/completion/shell/powershell.ts +66 -0
- package/src/service/completion/shell/zsh.ts +50 -0
- package/src/service/configuration/ConfigurationServiceProvider.ts +138 -42
- package/src/service/configuration/DefaultKeyValueService.ts +53 -7
- package/src/service/configuration/DefaultSecretService.ts +85 -0
- package/src/service/dataDumpGenerator/DataDumpGeneratorServiceProvider.ts +41 -0
- package/src/service/dataDumpGenerator/DefaultDataDumpGeneratorService.ts +80 -0
- package/src/service/imagePrinter/DefaultImagePrinterService.ts +23 -0
- package/src/service/imagePrinter/ImagePrinterServiceProvider.ts +32 -0
- package/src/service/prettyPrinter/PrettyPrinterServiceProvider.ts +1 -1
- package/src/service/printer/DefaultPrinterService.ts +147 -27
- package/src/service/printer/PrinterServiceProvider.ts +7 -7
- package/src/service/printer/command/NoHyperlinksCommand.ts +40 -0
- package/src/service/printer/terminal/Progress.ts +21 -4
- package/src/service/printer/terminal/Spinner.ts +32 -8
- package/src/service/printer/terminal/Theme.ts +1 -1
- package/src/service/prompter/DefaultPrompterService.ts +87 -0
- package/src/service/prompter/PrompterServiceProvider.ts +36 -0
- package/src/service/prompter/command/NoPromptCommand.ts +40 -0
- package/src/service/prompter/prompt/PromptContext.ts +47 -0
- package/src/service/prompter/prompt/promptAcknowledge.ts +28 -0
- package/src/service/prompter/prompt/promptMultiSelect.ts +99 -0
- package/src/service/prompter/prompt/promptOpenUrl.ts +97 -0
- package/src/service/prompter/prompt/promptSingleSelect.ts +102 -0
- package/src/service/prompter/prompt/promptText.ts +107 -0
- package/src/service/prompter/prompt/promptToggle.ts +28 -0
- package/src/service/shutdown/DefaultShutdownService.ts +13 -0
- package/src/service/shutdown/ShutdownServiceProvider.ts +28 -9
- package/src/service/shutdown/ShutdownState.ts +5 -0
- package/src/service/syntaxHighlighter/SyntaxHighlighterServiceProvider.ts +1 -1
- package/src/service/tableGenerator/DefaultTableGeneratorService.ts +415 -0
- package/src/service/tableGenerator/TableGeneratorServiceProvider.ts +43 -0
- package/src/service/treePrinter/DefaultTreePrinterService.ts +49 -0
- package/src/service/treePrinter/TreePrinterServiceProvider.ts +40 -0
- package/src/terminal/Ansi.ts +27 -0
- package/src/terminal/ImageRenderer.ts +199 -0
- package/src/terminal/KeyReader.ts +23 -0
- package/src/terminal/PngDecoder.ts +143 -0
- package/src/{service/printer/terminal → terminal}/Styler.ts +3 -0
- package/src/{service/printer/terminal → terminal}/Terminal.ts +1 -0
- package/src/terminal/TtyKeyReader.ts +83 -0
- package/src/terminal/TtyStyler.ts +78 -0
- package/src/{service/printer/terminal → terminal}/TtyTerminal.ts +9 -1
- package/src/terminal/supportsHyperlinks.ts +110 -0
- package/src/util/configHelper.ts +2 -2
- package/src/util/envVarHelper.ts +2 -2
- package/src/util/helpHelper.ts +66 -122
- package/src/util/logger.ts +16 -14
- package/src/util/runnerHelper.ts +1 -1
- package/tests/cli/BaseCLI.test.ts +50 -54
- package/tests/command/CompletionCommand.test.ts +68 -0
- package/tests/command/UsageCommand.test.ts +3 -2
- package/tests/command/VersionCommand.test.ts +3 -2
- package/tests/fixtures/Context.ts +9 -2
- package/tests/fixtures/util.ts +1 -1
- package/tests/runtime/parser.test.ts +9 -7
- package/tests/runtime/registry/DefaultServiceProviderRegistry.test.ts +3 -3
- package/tests/runtime/values/argumentValueValidation.test.ts +193 -0
- package/tests/service/argumentPrompter/ArgumentPrompterServiceProvider.test.ts +47 -0
- package/tests/service/argumentPrompter/DefaultArgumentPrompterService.test.ts +754 -0
- package/tests/service/asciiBannerGenerator/AsciiBannerGeneratorServiceProvider.test.ts +5 -4
- package/tests/service/asciiBannerGenerator/DefaultAsciiBannerGeneratorService.test.ts +163 -3
- package/tests/service/asciiBannerGenerator/__snapshots__/DefaultAsciiBannerGeneratorService.test.ts.snap +1 -1
- package/tests/service/banner/BannerServiceProvider.test.ts +8 -4
- package/tests/service/chiselAsciiBannerGenerator/ChiselFontAsciiBannerGeneratorService.test.ts +52 -0
- package/tests/service/completion/CompletionServiceProvider.test.ts +404 -0
- package/tests/service/completion/DefaultCompletionService.test.ts +278 -0
- package/tests/service/completion/shell/bash.test.ts +73 -0
- package/tests/service/completion/shell/fish.test.ts +62 -0
- package/tests/service/completion/shell/powershell.test.ts +59 -0
- package/tests/service/completion/shell/zsh.test.ts +60 -0
- package/tests/service/configuration/ConfigurationServiceProvider.test.ts +24 -12
- package/tests/service/configuration/DefaultKeyValueService.test.ts +118 -16
- package/tests/service/configuration/DefaultSecretService.test.ts +148 -0
- package/tests/service/configuration/command/ConfigCommand.test.ts +29 -0
- package/tests/service/configuration/command/DumpConfigCommand.test.ts +60 -0
- package/tests/service/dataDumpGenerator/DataDumpGeneratorServiceProvider.test.ts +57 -0
- package/tests/service/dataDumpGenerator/DefaultDataDumpGeneratorService.test.ts +217 -0
- package/tests/service/imagePrinter/DefaultImagePrinterService.test.ts +96 -0
- package/tests/service/imagePrinter/ImagePrinterServiceProvider.test.ts +17 -0
- package/tests/service/prettyPrinter/DefaultPrettyPrinterService.test.ts +2 -2
- package/tests/service/prettyPrinter/PrettyPrinterServiceProvider.test.ts +5 -4
- package/tests/service/printer/DefaultPrinterService.test.ts +233 -2
- package/tests/service/printer/PrinterServiceProvider.test.ts +6 -5
- package/tests/service/printer/command/DarkModeCommand.test.ts +83 -0
- package/tests/service/printer/command/LogLevelCommand.test.ts +7 -5
- package/tests/service/printer/command/NoColorCommand.test.ts +83 -0
- package/tests/service/printer/command/NoHyperlinksCommand.test.ts +89 -0
- package/tests/service/printer/terminal/Progress.test.ts +64 -4
- package/tests/service/printer/terminal/Spinner.test.ts +44 -3
- package/tests/service/prompter/DefaultPrompterService.test.ts +615 -0
- package/tests/service/prompter/PrompterServiceProvider.test.ts +47 -0
- package/tests/service/prompter/command/NoPromptCommand.test.ts +58 -0
- package/tests/service/shutdown/ShutdownServiceProvider.test.ts +4 -2
- package/tests/service/syntaxHighlighter/DefaultSyntaxHighlighterService.test.ts +3 -2
- package/tests/service/syntaxHighlighter/SyntaxHighlighterServiceProvider.test.ts +5 -4
- package/tests/service/tableGenerator/DefaultTableGeneratorService.test.ts +363 -0
- package/tests/service/tableGenerator/TableGeneratorServiceProvider.test.ts +47 -0
- package/tests/service/treePrinter/DefaultTreePrinterService.test.ts +144 -0
- package/tests/service/treePrinter/TreePrinterServiceProvider.test.ts +55 -0
- package/tests/terminal/ImageRenderer.test.ts +161 -0
- package/tests/terminal/PngDecoder.test.ts +223 -0
- package/tests/terminal/supportsHyperlinks.test.ts +149 -0
- package/tests/util/configHelper.test.ts +4 -4
- package/tests/util/logger.test.ts +23 -1
- package/tsconfig.json +31 -0
- package/src/service/printer/terminal/Ansi.ts +0 -10
- package/src/service/printer/terminal/TtyStyler.ts +0 -39
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type ShutdownService from "../../api/service/core/ShutdownService.ts";
|
|
2
|
+
import { shutdownState } from "./ShutdownState.ts";
|
|
2
3
|
|
|
3
4
|
export default class DefaultShutdownService implements ShutdownService {
|
|
4
5
|
static readonly callbacks: Array<() => Promise<void>> = [];
|
|
@@ -6,4 +7,16 @@ export default class DefaultShutdownService implements ShutdownService {
|
|
|
6
7
|
addShutdownListener(callback: () => Promise<void>): void {
|
|
7
8
|
DefaultShutdownService.callbacks.push(callback);
|
|
8
9
|
}
|
|
10
|
+
|
|
11
|
+
enterLongRunningMode(): void {
|
|
12
|
+
shutdownState.longRunningMode = true;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
leaveLongRunningMode(): void {
|
|
16
|
+
shutdownState.longRunningMode = false;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
get isShutdownRequested(): boolean {
|
|
20
|
+
return shutdownState.shutdownRequested;
|
|
21
|
+
}
|
|
9
22
|
}
|
|
@@ -9,29 +9,25 @@ import { SHUTDOWN_SERVICE_ID } from "../../api/service/core/ShutdownService.ts";
|
|
|
9
9
|
import type Context from "../../api/Context.ts";
|
|
10
10
|
import getLogger from "../../util/logger.ts";
|
|
11
11
|
import type CLIConfig from "../../api/CLIConfig.ts";
|
|
12
|
+
import { shutdownState } from "./ShutdownState.ts";
|
|
12
13
|
|
|
13
14
|
const logger = getLogger("ShutdownServiceProvider");
|
|
14
15
|
|
|
15
|
-
/**
|
|
16
|
-
* Provides a {@link ShutdownService}.
|
|
17
|
-
*/
|
|
18
16
|
export default class ShutdownServiceProvider implements ServiceProvider {
|
|
19
17
|
readonly serviceId: string = SHUTDOWN_SERVICE_ID;
|
|
20
18
|
readonly #shutdownService: ShutdownService;
|
|
19
|
+
static #shutdownInProgress = false;
|
|
21
20
|
|
|
22
|
-
/**
|
|
23
|
-
* Create an instance of the service provider with the specified details.
|
|
24
|
-
*
|
|
25
|
-
* @param servicePriority the priority of the service.
|
|
26
|
-
*/
|
|
27
21
|
public constructor(
|
|
28
22
|
readonly servicePriority: number,
|
|
29
23
|
) {
|
|
30
24
|
this.#shutdownService = new DefaultShutdownService();
|
|
31
25
|
process.on("beforeExit", ShutdownServiceProvider.shutdown);
|
|
26
|
+
process.on("SIGINT", ShutdownServiceProvider.onInterrupt);
|
|
27
|
+
process.on("SIGTERM", ShutdownServiceProvider.onTerminate);
|
|
32
28
|
}
|
|
33
29
|
|
|
34
|
-
public
|
|
30
|
+
public getServiceInfo(_cliConfig: CLIConfig): Promise<ServiceInfo> {
|
|
35
31
|
return Promise.resolve({
|
|
36
32
|
service: this.#shutdownService,
|
|
37
33
|
commands: [],
|
|
@@ -42,9 +38,32 @@ export default class ShutdownServiceProvider implements ServiceProvider {
|
|
|
42
38
|
return Promise.resolve(undefined);
|
|
43
39
|
}
|
|
44
40
|
|
|
41
|
+
static onInterrupt(): void {
|
|
42
|
+
shutdownState.interruptCount++;
|
|
43
|
+
if (
|
|
44
|
+
!shutdownState.longRunningMode || shutdownState.interruptCount >= 3
|
|
45
|
+
) {
|
|
46
|
+
ShutdownServiceProvider.shutdown().then(() => {
|
|
47
|
+
process.exit(130);
|
|
48
|
+
});
|
|
49
|
+
} else {
|
|
50
|
+
shutdownState.shutdownRequested = true;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
static onTerminate(): void {
|
|
55
|
+
ShutdownServiceProvider.shutdown().then(() => {
|
|
56
|
+
process.exit(143);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
45
60
|
static async shutdown() {
|
|
61
|
+
if (ShutdownServiceProvider.#shutdownInProgress) return;
|
|
62
|
+
ShutdownServiceProvider.#shutdownInProgress = true;
|
|
46
63
|
try {
|
|
47
64
|
process.removeListener("beforeExit", ShutdownServiceProvider.shutdown);
|
|
65
|
+
process.removeListener("SIGINT", ShutdownServiceProvider.onInterrupt);
|
|
66
|
+
process.removeListener("SIGTERM", ShutdownServiceProvider.onTerminate);
|
|
48
67
|
for await (const callback of DefaultShutdownService.callbacks) {
|
|
49
68
|
await callback();
|
|
50
69
|
}
|
|
@@ -31,7 +31,7 @@ export default class SyntaxHighlighterServiceProvider
|
|
|
31
31
|
new DefaultSyntaxHighlighterService();
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
public
|
|
34
|
+
public getServiceInfo(_cliConfig: CLIConfig): Promise<ServiceInfo> {
|
|
35
35
|
return Promise.resolve({
|
|
36
36
|
service: this.#defaultSyntaxHighlighterService,
|
|
37
37
|
commands: [],
|
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
import type TableGeneratorService from "../../api/service/core/TableGeneratorService.ts";
|
|
2
|
+
import { Align } from "../../api/service/core/TableGeneratorService.ts";
|
|
3
|
+
import type Table from "../../api/service/core/Table.ts";
|
|
4
|
+
|
|
5
|
+
const BORDER_TOP_LEFT = "┌";
|
|
6
|
+
const BORDER_TOP = "─";
|
|
7
|
+
const BORDER_TOP_MIDDLE = "┬";
|
|
8
|
+
const BORDER_TOP_RIGHT = "┐";
|
|
9
|
+
const BORDER_LEFT = "│";
|
|
10
|
+
const BORDER_MIDDLE_VERTICAL = "│";
|
|
11
|
+
const BORDER_RIGHT = "│";
|
|
12
|
+
const BORDER_LEFT_MIDDLE = "├";
|
|
13
|
+
const BORDER_MIDDLE = "─";
|
|
14
|
+
const BORDER_MIDDLE_MIDDLE = "┼";
|
|
15
|
+
const BORDER_RIGHT_MIDDLE = "┤";
|
|
16
|
+
const BORDER_BOTTOM_LEFT = "└";
|
|
17
|
+
const BORDER_BOTTOM = "─";
|
|
18
|
+
const BORDER_BOTTOM_MIDDLE = "┴";
|
|
19
|
+
const BORDER_BOTTOM_RIGHT = "┘";
|
|
20
|
+
|
|
21
|
+
export default class DefaultTableGeneratorService
|
|
22
|
+
implements TableGeneratorService {
|
|
23
|
+
colorEnabled = true;
|
|
24
|
+
colorFunction: (text: string, hexFormattedColor: string) => string = (
|
|
25
|
+
text,
|
|
26
|
+
) => text;
|
|
27
|
+
backgroundColorFunction: (text: string, hexFormattedColor: string) => string =
|
|
28
|
+
(text) => text;
|
|
29
|
+
|
|
30
|
+
#resolveAlign(
|
|
31
|
+
table: Table,
|
|
32
|
+
rowIndex: number,
|
|
33
|
+
columnIndex: number,
|
|
34
|
+
): Align {
|
|
35
|
+
const cellOpts = table.getCell(rowIndex, columnIndex)?.options;
|
|
36
|
+
if (cellOpts?.align !== undefined) return cellOpts.align;
|
|
37
|
+
const rowOpts = table.getRowOptions(rowIndex);
|
|
38
|
+
if (rowOpts?.align !== undefined) return rowOpts.align;
|
|
39
|
+
const colOpts = table.getColumnOptions(columnIndex);
|
|
40
|
+
if (colOpts?.align !== undefined) return colOpts.align;
|
|
41
|
+
return table.options.align ?? Align.LEFT;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
#calculateColumnWidths(table: Table): number[] {
|
|
45
|
+
const border = table.options.border !== false;
|
|
46
|
+
const padding = table.options.padding ?? 1;
|
|
47
|
+
const maxWidth = table.options.maxWidth ?? 80;
|
|
48
|
+
|
|
49
|
+
const borderOverhead = border
|
|
50
|
+
? table.columnCount + 1
|
|
51
|
+
: Math.max(table.columnCount - 1, 0);
|
|
52
|
+
const paddingOverhead = table.columnCount * 2 * padding;
|
|
53
|
+
const contentBudget = Math.max(
|
|
54
|
+
maxWidth - borderOverhead - paddingOverhead,
|
|
55
|
+
table.columnCount,
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
const naturalWidths: number[] = [];
|
|
59
|
+
for (let col = 0; col < table.columnCount; col++) {
|
|
60
|
+
let maxCellWidth = 0;
|
|
61
|
+
for (let row = 0; row < table.rowCount; row++) {
|
|
62
|
+
const cell = table.getCell(row, col);
|
|
63
|
+
if (cell) {
|
|
64
|
+
const lines = cell.contents.split("\n");
|
|
65
|
+
for (const line of lines) {
|
|
66
|
+
const w = Bun.stringWidth(line);
|
|
67
|
+
if (w > maxCellWidth) maxCellWidth = w;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const colOpts = table.getColumnOptions(col);
|
|
72
|
+
if (colOpts?.minWidth !== undefined && maxCellWidth < colOpts.minWidth) {
|
|
73
|
+
maxCellWidth = colOpts.minWidth;
|
|
74
|
+
}
|
|
75
|
+
if (colOpts?.maxWidth !== undefined && maxCellWidth > colOpts.maxWidth) {
|
|
76
|
+
maxCellWidth = colOpts.maxWidth;
|
|
77
|
+
}
|
|
78
|
+
naturalWidths.push(maxCellWidth);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const totalNatural = naturalWidths.reduce((a, b) => a + b, 0);
|
|
82
|
+
|
|
83
|
+
if (totalNatural <= contentBudget) {
|
|
84
|
+
const surplus = contentBudget - totalNatural;
|
|
85
|
+
const totalFlexWeight = this.#sumFlexWeights(table);
|
|
86
|
+
if (surplus > 0 && totalFlexWeight > 0) {
|
|
87
|
+
this.#distributeGrow(table, naturalWidths, surplus, totalFlexWeight);
|
|
88
|
+
}
|
|
89
|
+
} else {
|
|
90
|
+
const excess = totalNatural - contentBudget;
|
|
91
|
+
this.#distributeShrink(table, naturalWidths, excess);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return naturalWidths;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
#sumFlexWeights(table: Table): number {
|
|
98
|
+
let total = 0;
|
|
99
|
+
for (let col = 0; col < table.columnCount; col++) {
|
|
100
|
+
const colOpts = table.getColumnOptions(col);
|
|
101
|
+
total += colOpts?.flexWeight ?? 0;
|
|
102
|
+
}
|
|
103
|
+
return total;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
#distributeGrow(
|
|
107
|
+
table: Table,
|
|
108
|
+
widths: number[],
|
|
109
|
+
surplus: number,
|
|
110
|
+
totalFlexWeight: number,
|
|
111
|
+
): void {
|
|
112
|
+
let remaining = surplus;
|
|
113
|
+
for (let col = 0; col < table.columnCount; col++) {
|
|
114
|
+
const colOpts = table.getColumnOptions(col);
|
|
115
|
+
const weight = colOpts?.flexWeight ?? 0;
|
|
116
|
+
if (weight <= 0) continue;
|
|
117
|
+
const share = Math.floor((weight / totalFlexWeight) * surplus);
|
|
118
|
+
let newWidth = widths[col]! + share;
|
|
119
|
+
if (colOpts?.maxWidth !== undefined && newWidth > colOpts.maxWidth) {
|
|
120
|
+
newWidth = colOpts.maxWidth;
|
|
121
|
+
}
|
|
122
|
+
const added = newWidth - widths[col]!;
|
|
123
|
+
widths[col] = newWidth;
|
|
124
|
+
remaining -= added;
|
|
125
|
+
}
|
|
126
|
+
// distribute any rounding remainder to first flex column
|
|
127
|
+
if (remaining > 0) {
|
|
128
|
+
for (let col = 0; col < table.columnCount; col++) {
|
|
129
|
+
const colOpts = table.getColumnOptions(col);
|
|
130
|
+
if ((colOpts?.flexWeight ?? 0) > 0) {
|
|
131
|
+
let newWidth = widths[col]! + remaining;
|
|
132
|
+
if (colOpts?.maxWidth !== undefined && newWidth > colOpts.maxWidth) {
|
|
133
|
+
newWidth = colOpts.maxWidth;
|
|
134
|
+
}
|
|
135
|
+
widths[col] = newWidth;
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
#distributeShrink(
|
|
143
|
+
table: Table,
|
|
144
|
+
widths: number[],
|
|
145
|
+
excess: number,
|
|
146
|
+
): void {
|
|
147
|
+
let remaining = excess;
|
|
148
|
+
const shrinkable = new Set<number>();
|
|
149
|
+
for (let col = 0; col < table.columnCount; col++) {
|
|
150
|
+
const colOpts = table.getColumnOptions(col);
|
|
151
|
+
if ((colOpts?.flexWeight ?? 0) > 0) {
|
|
152
|
+
shrinkable.add(col);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
while (remaining > 0 && shrinkable.size > 0) {
|
|
157
|
+
let totalWeightedWidth = 0;
|
|
158
|
+
for (const col of shrinkable) {
|
|
159
|
+
const colOpts = table.getColumnOptions(col);
|
|
160
|
+
totalWeightedWidth += (colOpts?.flexWeight ?? 0) * widths[col]!;
|
|
161
|
+
}
|
|
162
|
+
if (totalWeightedWidth <= 0) break;
|
|
163
|
+
|
|
164
|
+
let shrank = false;
|
|
165
|
+
for (const col of shrinkable) {
|
|
166
|
+
if (remaining <= 0) break;
|
|
167
|
+
const colOpts = table.getColumnOptions(col);
|
|
168
|
+
const weight = colOpts?.flexWeight ?? 0;
|
|
169
|
+
const share = Math.max(
|
|
170
|
+
1,
|
|
171
|
+
Math.floor(
|
|
172
|
+
(weight * widths[col]! / totalWeightedWidth) * remaining,
|
|
173
|
+
),
|
|
174
|
+
);
|
|
175
|
+
const minWidth = colOpts?.minWidth ?? 1;
|
|
176
|
+
const maxShrink = widths[col]! - minWidth;
|
|
177
|
+
if (maxShrink <= 0) {
|
|
178
|
+
shrinkable.delete(col);
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
const actual = Math.min(share, maxShrink);
|
|
182
|
+
widths[col] = widths[col]! - actual;
|
|
183
|
+
remaining -= actual;
|
|
184
|
+
shrank = true;
|
|
185
|
+
if (widths[col]! <= minWidth) {
|
|
186
|
+
shrinkable.delete(col);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (!shrank) break;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
#wrapLine(line: string, width: number): string[] {
|
|
194
|
+
if (width <= 0) return [line];
|
|
195
|
+
if (Bun.stringWidth(line) <= width) return [line];
|
|
196
|
+
|
|
197
|
+
const wrapped: string[] = [];
|
|
198
|
+
const words = line.split(/(\s+)/);
|
|
199
|
+
let currentLine = "";
|
|
200
|
+
let currentWidth = 0;
|
|
201
|
+
|
|
202
|
+
for (const word of words) {
|
|
203
|
+
const wordWidth = Bun.stringWidth(word);
|
|
204
|
+
if (currentWidth === 0) {
|
|
205
|
+
if (wordWidth <= width) {
|
|
206
|
+
currentLine = word;
|
|
207
|
+
currentWidth = wordWidth;
|
|
208
|
+
} else {
|
|
209
|
+
this.#hardBreak(word, width, wrapped);
|
|
210
|
+
currentLine = "";
|
|
211
|
+
currentWidth = 0;
|
|
212
|
+
if (wrapped.length > 0) {
|
|
213
|
+
const last = wrapped[wrapped.length - 1]!;
|
|
214
|
+
if (Bun.stringWidth(last) < width) {
|
|
215
|
+
currentLine = wrapped.pop()!;
|
|
216
|
+
currentWidth = Bun.stringWidth(currentLine);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
} else if (currentWidth + wordWidth <= width) {
|
|
221
|
+
currentLine += word;
|
|
222
|
+
currentWidth += wordWidth;
|
|
223
|
+
} else {
|
|
224
|
+
wrapped.push(currentLine);
|
|
225
|
+
const trimmed = word.replace(/^\s+/, "");
|
|
226
|
+
const trimmedWidth = Bun.stringWidth(trimmed);
|
|
227
|
+
if (trimmedWidth <= width) {
|
|
228
|
+
currentLine = trimmed;
|
|
229
|
+
currentWidth = trimmedWidth;
|
|
230
|
+
} else {
|
|
231
|
+
this.#hardBreak(trimmed, width, wrapped);
|
|
232
|
+
currentLine = "";
|
|
233
|
+
currentWidth = 0;
|
|
234
|
+
if (wrapped.length > 0) {
|
|
235
|
+
const last = wrapped[wrapped.length - 1]!;
|
|
236
|
+
if (Bun.stringWidth(last) < width) {
|
|
237
|
+
currentLine = wrapped.pop()!;
|
|
238
|
+
currentWidth = Bun.stringWidth(currentLine);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
if (currentLine.length > 0) {
|
|
245
|
+
wrapped.push(currentLine);
|
|
246
|
+
}
|
|
247
|
+
return wrapped.length > 0 ? wrapped : [""];
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
#hardBreak(text: string, width: number, output: string[]): void {
|
|
251
|
+
let remaining = text;
|
|
252
|
+
while (Bun.stringWidth(remaining) > width) {
|
|
253
|
+
let breakAt = 0;
|
|
254
|
+
let w = 0;
|
|
255
|
+
for (const char of remaining) {
|
|
256
|
+
const cw = Bun.stringWidth(char);
|
|
257
|
+
if (w + cw > width) break;
|
|
258
|
+
w += cw;
|
|
259
|
+
breakAt += char.length;
|
|
260
|
+
}
|
|
261
|
+
if (breakAt === 0) breakAt = 1;
|
|
262
|
+
output.push(remaining.slice(0, breakAt));
|
|
263
|
+
remaining = remaining.slice(breakAt);
|
|
264
|
+
}
|
|
265
|
+
if (remaining.length > 0) {
|
|
266
|
+
output.push(remaining);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
#wrapCellContent(contents: string, width: number): string[] {
|
|
271
|
+
const inputLines = contents.split("\n");
|
|
272
|
+
const result: string[] = [];
|
|
273
|
+
for (const line of inputLines) {
|
|
274
|
+
result.push(...this.#wrapLine(line, width));
|
|
275
|
+
}
|
|
276
|
+
return result.length > 0 ? result : [""];
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
#alignText(text: string, width: number, align: Align): string {
|
|
280
|
+
const textWidth = Bun.stringWidth(text);
|
|
281
|
+
const gap = width - textWidth;
|
|
282
|
+
if (gap <= 0) return text;
|
|
283
|
+
switch (align) {
|
|
284
|
+
case Align.LEFT:
|
|
285
|
+
return text + " ".repeat(gap);
|
|
286
|
+
case Align.RIGHT:
|
|
287
|
+
return " ".repeat(gap) + text;
|
|
288
|
+
case Align.CENTER: {
|
|
289
|
+
const left = Math.floor(gap / 2);
|
|
290
|
+
const right = gap - left;
|
|
291
|
+
return " ".repeat(left) + text + " ".repeat(right);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
#colorBorder(char: string, table: Table): string {
|
|
297
|
+
let result = char;
|
|
298
|
+
if (this.colorEnabled && table.options.borderBackgroundColor) {
|
|
299
|
+
result = this.backgroundColorFunction(
|
|
300
|
+
result,
|
|
301
|
+
table.options.borderBackgroundColor,
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
if (this.colorEnabled && table.options.borderColor) {
|
|
305
|
+
result = this.colorFunction(result, table.options.borderColor);
|
|
306
|
+
}
|
|
307
|
+
return result;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
render(table: Table): string {
|
|
311
|
+
const border = table.options.border !== false;
|
|
312
|
+
const padding = table.options.padding ?? 1;
|
|
313
|
+
const columnWidths = this.#calculateColumnWidths(table);
|
|
314
|
+
const pad = " ".repeat(padding);
|
|
315
|
+
|
|
316
|
+
const wrappedCells: string[][][] = [];
|
|
317
|
+
const rowHeights: number[] = [];
|
|
318
|
+
for (let row = 0; row < table.rowCount; row++) {
|
|
319
|
+
const rowCells: string[][] = [];
|
|
320
|
+
let maxHeight = 1;
|
|
321
|
+
for (let col = 0; col < table.columnCount; col++) {
|
|
322
|
+
const cell = table.getCell(row, col);
|
|
323
|
+
const lines = this.#wrapCellContent(
|
|
324
|
+
cell?.contents ?? "",
|
|
325
|
+
columnWidths[col]!,
|
|
326
|
+
);
|
|
327
|
+
rowCells.push(lines);
|
|
328
|
+
if (lines.length > maxHeight) maxHeight = lines.length;
|
|
329
|
+
}
|
|
330
|
+
wrappedCells.push(rowCells);
|
|
331
|
+
rowHeights.push(maxHeight);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const output: string[] = [];
|
|
335
|
+
|
|
336
|
+
if (border) {
|
|
337
|
+
output.push(this.#renderHorizontalBorder(
|
|
338
|
+
table,
|
|
339
|
+
columnWidths,
|
|
340
|
+
padding,
|
|
341
|
+
BORDER_TOP_LEFT,
|
|
342
|
+
BORDER_TOP,
|
|
343
|
+
BORDER_TOP_MIDDLE,
|
|
344
|
+
BORDER_TOP_RIGHT,
|
|
345
|
+
));
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
for (let row = 0; row < table.rowCount; row++) {
|
|
349
|
+
for (let subLine = 0; subLine < rowHeights[row]!; subLine++) {
|
|
350
|
+
let line = "";
|
|
351
|
+
if (border) line += this.#colorBorder(BORDER_LEFT, table);
|
|
352
|
+
for (let col = 0; col < table.columnCount; col++) {
|
|
353
|
+
if (!border && col > 0) line += " ".repeat(padding * 2);
|
|
354
|
+
const cellLines = wrappedCells[row]![col]!;
|
|
355
|
+
const content = subLine < cellLines.length ? cellLines[subLine]! : "";
|
|
356
|
+
const align = this.#resolveAlign(table, row, col);
|
|
357
|
+
const aligned = this.#alignText(content, columnWidths[col]!, align);
|
|
358
|
+
line += pad + aligned + pad;
|
|
359
|
+
if (border && col < table.columnCount - 1) {
|
|
360
|
+
line += this.#colorBorder(BORDER_MIDDLE_VERTICAL, table);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
if (border) line += this.#colorBorder(BORDER_RIGHT, table);
|
|
364
|
+
output.push(line);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (border && row < table.rowCount - 1) {
|
|
368
|
+
output.push(this.#renderHorizontalBorder(
|
|
369
|
+
table,
|
|
370
|
+
columnWidths,
|
|
371
|
+
padding,
|
|
372
|
+
BORDER_LEFT_MIDDLE,
|
|
373
|
+
BORDER_MIDDLE,
|
|
374
|
+
BORDER_MIDDLE_MIDDLE,
|
|
375
|
+
BORDER_RIGHT_MIDDLE,
|
|
376
|
+
));
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (border) {
|
|
381
|
+
output.push(this.#renderHorizontalBorder(
|
|
382
|
+
table,
|
|
383
|
+
columnWidths,
|
|
384
|
+
padding,
|
|
385
|
+
BORDER_BOTTOM_LEFT,
|
|
386
|
+
BORDER_BOTTOM,
|
|
387
|
+
BORDER_BOTTOM_MIDDLE,
|
|
388
|
+
BORDER_BOTTOM_RIGHT,
|
|
389
|
+
));
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return output.join("\n");
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
#renderHorizontalBorder(
|
|
396
|
+
table: Table,
|
|
397
|
+
columnWidths: number[],
|
|
398
|
+
padding: number,
|
|
399
|
+
left: string,
|
|
400
|
+
horizontal: string,
|
|
401
|
+
middle: string,
|
|
402
|
+
right: string,
|
|
403
|
+
): string {
|
|
404
|
+
let line = this.#colorBorder(left, table);
|
|
405
|
+
for (let col = 0; col < table.columnCount; col++) {
|
|
406
|
+
const segment = horizontal.repeat(columnWidths[col]! + padding * 2);
|
|
407
|
+
line += this.#colorBorder(segment, table);
|
|
408
|
+
if (col < table.columnCount - 1) {
|
|
409
|
+
line += this.#colorBorder(middle, table);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
line += this.#colorBorder(right, table);
|
|
413
|
+
return line;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ServiceInfo,
|
|
3
|
+
ServiceProvider,
|
|
4
|
+
} from "../../api/service/ServiceProvider.ts";
|
|
5
|
+
import DefaultTableGeneratorService from "./DefaultTableGeneratorService.ts";
|
|
6
|
+
import { TABLE_GENERATOR_SERVICE_ID } from "../../api/service/core/TableGeneratorService.ts";
|
|
7
|
+
import type Context from "../../api/Context.ts";
|
|
8
|
+
import type PrinterService from "../../api/service/core/PrinterService.ts";
|
|
9
|
+
import { PRINTER_SERVICE_ID } from "../../api/service/core/PrinterService.ts";
|
|
10
|
+
import type CLIConfig from "../../api/CLIConfig.ts";
|
|
11
|
+
|
|
12
|
+
export default class TableGeneratorServiceProvider implements ServiceProvider {
|
|
13
|
+
readonly serviceId: string = TABLE_GENERATOR_SERVICE_ID;
|
|
14
|
+
readonly #defaultTableGeneratorService: DefaultTableGeneratorService;
|
|
15
|
+
|
|
16
|
+
public constructor(
|
|
17
|
+
readonly servicePriority: number,
|
|
18
|
+
) {
|
|
19
|
+
this.#defaultTableGeneratorService = new DefaultTableGeneratorService();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
public getServiceInfo(_cliConfig: CLIConfig): Promise<ServiceInfo> {
|
|
23
|
+
return Promise.resolve({
|
|
24
|
+
service: this.#defaultTableGeneratorService,
|
|
25
|
+
commands: [],
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
initService(context: Context): Promise<void> {
|
|
30
|
+
const printerService = context.getServiceById(
|
|
31
|
+
PRINTER_SERVICE_ID,
|
|
32
|
+
) as PrinterService;
|
|
33
|
+
|
|
34
|
+
this.#defaultTableGeneratorService.colorEnabled =
|
|
35
|
+
printerService.colorEnabled;
|
|
36
|
+
this.#defaultTableGeneratorService.colorFunction = printerService.color
|
|
37
|
+
.bind(printerService);
|
|
38
|
+
this.#defaultTableGeneratorService.backgroundColorFunction = printerService
|
|
39
|
+
.backgroundColor.bind(printerService);
|
|
40
|
+
|
|
41
|
+
return Promise.resolve(undefined);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type TreePrinterService from "../../api/service/core/TreePrinterService.ts";
|
|
2
|
+
import type { TreeNode } from "../../api/service/core/TreePrinterService.ts";
|
|
3
|
+
|
|
4
|
+
const DEFAULT_NODE_COLOR = "#839496";
|
|
5
|
+
|
|
6
|
+
export default class DefaultTreePrinterService implements TreePrinterService {
|
|
7
|
+
colorEnabled = true;
|
|
8
|
+
colorFunction: (text: string, hexFormattedColor: string) => string = (text) =>
|
|
9
|
+
text;
|
|
10
|
+
|
|
11
|
+
readonly #nodeColor: string;
|
|
12
|
+
|
|
13
|
+
constructor(nodeColor: string = DEFAULT_NODE_COLOR) {
|
|
14
|
+
this.#nodeColor = nodeColor;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
#colorLabel(label: string): string {
|
|
18
|
+
if (this.colorEnabled) {
|
|
19
|
+
return this.colorFunction(label, this.#nodeColor);
|
|
20
|
+
}
|
|
21
|
+
return label;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
#renderChildren(
|
|
25
|
+
children: Array<TreeNode | string>,
|
|
26
|
+
prefix: string,
|
|
27
|
+
lines: string[],
|
|
28
|
+
): void {
|
|
29
|
+
for (let i = 0; i < children.length; i++) {
|
|
30
|
+
const child = children[i]!;
|
|
31
|
+
const isLast = i === children.length - 1;
|
|
32
|
+
const connector = isLast ? "└── " : "├── ";
|
|
33
|
+
const extension = isLast ? " " : "│ ";
|
|
34
|
+
|
|
35
|
+
if (typeof child === "string") {
|
|
36
|
+
lines.push(prefix + connector + this.#colorLabel(child));
|
|
37
|
+
} else {
|
|
38
|
+
lines.push(prefix + connector + this.#colorLabel(child.label));
|
|
39
|
+
this.#renderChildren(child.children ?? [], prefix + extension, lines);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
print(node: TreeNode): string {
|
|
45
|
+
const lines: string[] = [this.#colorLabel(node.label)];
|
|
46
|
+
this.#renderChildren(node.children ?? [], "", lines);
|
|
47
|
+
return lines.join("\n");
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ServiceInfo,
|
|
3
|
+
ServiceProvider,
|
|
4
|
+
} from "../../api/service/ServiceProvider.ts";
|
|
5
|
+
import DefaultTreePrinterService from "./DefaultTreePrinterService.ts";
|
|
6
|
+
import { TREE_PRINTER_SERVICE_ID } from "../../api/service/core/TreePrinterService.ts";
|
|
7
|
+
import type Context from "../../api/Context.ts";
|
|
8
|
+
import type PrinterService from "../../api/service/core/PrinterService.ts";
|
|
9
|
+
import { PRINTER_SERVICE_ID } from "../../api/service/core/PrinterService.ts";
|
|
10
|
+
import type CLIConfig from "../../api/CLIConfig.ts";
|
|
11
|
+
|
|
12
|
+
export default class TreePrinterServiceProvider implements ServiceProvider {
|
|
13
|
+
readonly serviceId: string = TREE_PRINTER_SERVICE_ID;
|
|
14
|
+
readonly #defaultTreePrinterService: DefaultTreePrinterService;
|
|
15
|
+
|
|
16
|
+
public constructor(
|
|
17
|
+
readonly servicePriority: number,
|
|
18
|
+
) {
|
|
19
|
+
this.#defaultTreePrinterService = new DefaultTreePrinterService();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
public getServiceInfo(_cliConfig: CLIConfig): Promise<ServiceInfo> {
|
|
23
|
+
return Promise.resolve({
|
|
24
|
+
service: this.#defaultTreePrinterService,
|
|
25
|
+
commands: [],
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
initService(context: Context): Promise<void> {
|
|
30
|
+
const printerService = context.getServiceById(
|
|
31
|
+
PRINTER_SERVICE_ID,
|
|
32
|
+
) as PrinterService;
|
|
33
|
+
|
|
34
|
+
this.#defaultTreePrinterService.colorEnabled = printerService.colorEnabled;
|
|
35
|
+
this.#defaultTreePrinterService.colorFunction = printerService.color
|
|
36
|
+
.bind(printerService);
|
|
37
|
+
|
|
38
|
+
return Promise.resolve(undefined);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
const CSI = "\x1b[";
|
|
2
|
+
const OSC = "\x1b]";
|
|
3
|
+
const BEL = "\x07";
|
|
4
|
+
const SEP = ";";
|
|
5
|
+
|
|
6
|
+
export const CLEAR_LINE = CSI + "2K";
|
|
7
|
+
export const CURSOR_LEFT = CSI + "G";
|
|
8
|
+
export const CURSOR_UP = CSI + "1A";
|
|
9
|
+
export const HIDE_CURSOR = CSI + "?25l";
|
|
10
|
+
export const SHOW_CURSOR = CSI + "?25h";
|
|
11
|
+
export const ITALIC_START = CSI + "3m";
|
|
12
|
+
export const ITALIC_END = CSI + "23m";
|
|
13
|
+
export const FOREGROUND_COLOR_END = CSI + "39m";
|
|
14
|
+
export const BACKGROUND_COLOR_END = CSI + "49m";
|
|
15
|
+
export const HYPERLINK_END = `${OSC}8${SEP}${SEP}${BEL}`;
|
|
16
|
+
|
|
17
|
+
export function foregroundColorStart(r: number, g: number, b: number): string {
|
|
18
|
+
return `\x1b[38;2;${r};${g};${b}m`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function backgroundColorStart(r: number, g: number, b: number): string {
|
|
22
|
+
return `\x1b[48;2;${r};${g};${b}m`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function hyperlinkStart(url: string): string {
|
|
26
|
+
return `${OSC}8${SEP}${SEP}${url}${BEL}`;
|
|
27
|
+
}
|