@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
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
import supportsTerminalGraphics from "supports-terminal-graphics";
|
|
3
|
+
import { decodePngToRGBA } from "./PngDecoder.ts";
|
|
4
|
+
import {
|
|
5
|
+
BACKGROUND_COLOR_END,
|
|
6
|
+
backgroundColorStart,
|
|
7
|
+
FOREGROUND_COLOR_END,
|
|
8
|
+
foregroundColorStart,
|
|
9
|
+
} from "./Ansi.ts";
|
|
10
|
+
import type Terminal from "./Terminal.ts";
|
|
11
|
+
import { Buffer } from "node:buffer";
|
|
12
|
+
|
|
13
|
+
export default class ImageRenderer {
|
|
14
|
+
#terminal: Terminal;
|
|
15
|
+
|
|
16
|
+
constructor(terminal: Terminal) {
|
|
17
|
+
this.#terminal = terminal;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
renderImage(
|
|
21
|
+
imageBuffer: Uint8Array,
|
|
22
|
+
widthPercentage: number = 100,
|
|
23
|
+
hexFormattedBackgroundColor?: string,
|
|
24
|
+
): Promise<string> {
|
|
25
|
+
if (!this.#isMultiplexer()) {
|
|
26
|
+
if (supportsTerminalGraphics.stdout.kitty) {
|
|
27
|
+
return this.#renderKitty(imageBuffer, widthPercentage);
|
|
28
|
+
}
|
|
29
|
+
if (supportsTerminalGraphics.stdout.iterm2) {
|
|
30
|
+
return this.#renderITerm2(imageBuffer, widthPercentage);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return this.#renderAnsiBlocks(
|
|
34
|
+
imageBuffer,
|
|
35
|
+
widthPercentage,
|
|
36
|
+
hexFormattedBackgroundColor,
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
#isMultiplexer(): boolean {
|
|
41
|
+
const term = (process.env.TERM || "").toLowerCase();
|
|
42
|
+
const termProgram = (process.env.TERM_PROGRAM || "").toLowerCase();
|
|
43
|
+
return termProgram === "tmux" || termProgram === "screen" ||
|
|
44
|
+
term.startsWith("tmux") || term.startsWith("screen");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// https://sw.kovidgoyal.net/kitty/graphics-protocol/
|
|
48
|
+
async #renderKitty(
|
|
49
|
+
imageBuffer: Uint8Array,
|
|
50
|
+
widthPercentage: number,
|
|
51
|
+
): Promise<string> {
|
|
52
|
+
const pngBytes = await new Bun.Image(imageBuffer).png().bytes();
|
|
53
|
+
const columns = Math.floor(
|
|
54
|
+
this.#terminal.columns() * widthPercentage / 100,
|
|
55
|
+
);
|
|
56
|
+
const base64 = Buffer.from(pngBytes).toString("base64");
|
|
57
|
+
const chunks: string[] = [];
|
|
58
|
+
for (let i = 0; i < base64.length; i += 4096) {
|
|
59
|
+
chunks.push(base64.slice(i, i + 4096));
|
|
60
|
+
}
|
|
61
|
+
let result = "";
|
|
62
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
63
|
+
const isLast = i === chunks.length - 1;
|
|
64
|
+
if (i === 0) {
|
|
65
|
+
result += `\x1b_Gf=100,a=T,c=${columns},m=${isLast ? 0 : 1};${
|
|
66
|
+
chunks[i]
|
|
67
|
+
}\x1b\\`;
|
|
68
|
+
} else {
|
|
69
|
+
result += `\x1b_Gm=${isLast ? 0 : 1};${chunks[i]}\x1b\\`;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// https://iterm2.com/documentation-images.html
|
|
76
|
+
async #renderITerm2(
|
|
77
|
+
imageBuffer: Uint8Array,
|
|
78
|
+
widthPercentage: number,
|
|
79
|
+
): Promise<string> {
|
|
80
|
+
const pngBytes = await new Bun.Image(imageBuffer).png().bytes();
|
|
81
|
+
const base64 = Buffer.from(pngBytes).toString("base64");
|
|
82
|
+
return `\x1b]1337;File=inline=1;width=${widthPercentage}%:${base64}\x07`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
static #parseHexColor(
|
|
86
|
+
hex: string,
|
|
87
|
+
): { r: number; g: number; b: number } | undefined {
|
|
88
|
+
const match = hex.match(/^#?([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i);
|
|
89
|
+
if (!match) return undefined;
|
|
90
|
+
return {
|
|
91
|
+
r: Number.parseInt(match[1]!, 16),
|
|
92
|
+
g: Number.parseInt(match[2]!, 16),
|
|
93
|
+
b: Number.parseInt(match[3]!, 16),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async #renderAnsiBlocks(
|
|
98
|
+
imageBuffer: Uint8Array,
|
|
99
|
+
widthPercentage: number,
|
|
100
|
+
hexFormattedBackgroundColor?: string,
|
|
101
|
+
): Promise<string> {
|
|
102
|
+
const bg = hexFormattedBackgroundColor
|
|
103
|
+
? ImageRenderer.#parseHexColor(hexFormattedBackgroundColor)
|
|
104
|
+
: undefined;
|
|
105
|
+
const columns = this.#terminal.columns();
|
|
106
|
+
const rows = this.#terminal.rows();
|
|
107
|
+
const targetWidth = Math.floor(columns * widthPercentage / 100);
|
|
108
|
+
|
|
109
|
+
const meta = await new Bun.Image(imageBuffer).metadata();
|
|
110
|
+
const aspectRatio = meta.height! / meta.width!;
|
|
111
|
+
let targetHeight = Math.round(targetWidth * aspectRatio);
|
|
112
|
+
if (targetHeight % 2 !== 0) targetHeight++;
|
|
113
|
+
const maxHeight = (rows - 2) * 2;
|
|
114
|
+
if (targetHeight > maxHeight) targetHeight = maxHeight;
|
|
115
|
+
|
|
116
|
+
const resizedPng = await new Bun.Image(imageBuffer)
|
|
117
|
+
.resize(targetWidth, targetHeight, { fit: "inside" })
|
|
118
|
+
.png()
|
|
119
|
+
.bytes();
|
|
120
|
+
const { width, height, pixels } = decodePngToRGBA(resizedPng);
|
|
121
|
+
|
|
122
|
+
const lines: string[] = [];
|
|
123
|
+
for (let y = 0; y < height; y += 2) {
|
|
124
|
+
let line = "";
|
|
125
|
+
const hasBottom = y + 1 < height;
|
|
126
|
+
for (let x = 0; x < width; x++) {
|
|
127
|
+
const topIdx = (y * width + x) * 4;
|
|
128
|
+
const r = pixels[topIdx]!;
|
|
129
|
+
const g = pixels[topIdx + 1]!;
|
|
130
|
+
const b = pixels[topIdx + 2]!;
|
|
131
|
+
const a = pixels[topIdx + 3]!;
|
|
132
|
+
|
|
133
|
+
if (hasBottom) {
|
|
134
|
+
const botIdx = ((y + 1) * width + x) * 4;
|
|
135
|
+
const r2 = pixels[botIdx]!;
|
|
136
|
+
const g2 = pixels[botIdx + 1]!;
|
|
137
|
+
const b2 = pixels[botIdx + 2]!;
|
|
138
|
+
const a2 = pixels[botIdx + 3]!;
|
|
139
|
+
|
|
140
|
+
if (a === 0 && a2 === 0) {
|
|
141
|
+
if (bg) {
|
|
142
|
+
line += backgroundColorStart(
|
|
143
|
+
bg.r,
|
|
144
|
+
bg.g,
|
|
145
|
+
bg.b,
|
|
146
|
+
) + " " + BACKGROUND_COLOR_END;
|
|
147
|
+
} else {
|
|
148
|
+
line += " ";
|
|
149
|
+
}
|
|
150
|
+
} else if (a === 0) {
|
|
151
|
+
if (bg) {
|
|
152
|
+
line += backgroundColorStart(
|
|
153
|
+
bg.r,
|
|
154
|
+
bg.g,
|
|
155
|
+
bg.b,
|
|
156
|
+
) + foregroundColorStart(r2, g2, b2) + "▄" +
|
|
157
|
+
FOREGROUND_COLOR_END + BACKGROUND_COLOR_END;
|
|
158
|
+
} else {
|
|
159
|
+
line += foregroundColorStart(r2, g2, b2) + "▄" +
|
|
160
|
+
FOREGROUND_COLOR_END;
|
|
161
|
+
}
|
|
162
|
+
} else if (a2 === 0) {
|
|
163
|
+
if (bg) {
|
|
164
|
+
line += backgroundColorStart(
|
|
165
|
+
bg.r,
|
|
166
|
+
bg.g,
|
|
167
|
+
bg.b,
|
|
168
|
+
) + foregroundColorStart(r, g, b) + "▀" +
|
|
169
|
+
FOREGROUND_COLOR_END + BACKGROUND_COLOR_END;
|
|
170
|
+
} else {
|
|
171
|
+
line += foregroundColorStart(r, g, b) + "▀" +
|
|
172
|
+
FOREGROUND_COLOR_END;
|
|
173
|
+
}
|
|
174
|
+
} else {
|
|
175
|
+
line += backgroundColorStart(r, g, b) +
|
|
176
|
+
foregroundColorStart(r2, g2, b2) + "▄" +
|
|
177
|
+
FOREGROUND_COLOR_END + BACKGROUND_COLOR_END;
|
|
178
|
+
}
|
|
179
|
+
} else {
|
|
180
|
+
if (a === 0) {
|
|
181
|
+
if (bg) {
|
|
182
|
+
line += backgroundColorStart(
|
|
183
|
+
bg.r,
|
|
184
|
+
bg.g,
|
|
185
|
+
bg.b,
|
|
186
|
+
) + " " + BACKGROUND_COLOR_END;
|
|
187
|
+
} else {
|
|
188
|
+
line += " ";
|
|
189
|
+
}
|
|
190
|
+
} else {
|
|
191
|
+
line += backgroundColorStart(r, g, b) + " " + BACKGROUND_COLOR_END;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
lines.push(line);
|
|
196
|
+
}
|
|
197
|
+
return lines.join("\n");
|
|
198
|
+
}
|
|
199
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export enum SpecialKey {
|
|
2
|
+
UP = 0,
|
|
3
|
+
DOWN = 1,
|
|
4
|
+
LEFT = 2,
|
|
5
|
+
RIGHT = 3,
|
|
6
|
+
ENTER = 4,
|
|
7
|
+
SPACE = 5,
|
|
8
|
+
BACKSPACE = 6,
|
|
9
|
+
ESCAPE = 7,
|
|
10
|
+
TAB = 8,
|
|
11
|
+
INTERRUPT = 9,
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface KeyEvent {
|
|
15
|
+
readonly key?: string;
|
|
16
|
+
readonly specialKey?: SpecialKey;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export default interface KeyReader {
|
|
20
|
+
enableRawMode(): void;
|
|
21
|
+
disableRawMode(): void;
|
|
22
|
+
readKey(): Promise<KeyEvent>;
|
|
23
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// Minimal PNG-to-RGBA pixel extractor. Supports 8-bit RGB and RGBA only.
|
|
2
|
+
import { inflateSync } from "node:zlib";
|
|
3
|
+
|
|
4
|
+
function paethPredictor(a: number, b: number, c: number): number {
|
|
5
|
+
const p = a + b - c;
|
|
6
|
+
const pa = Math.abs(p - a);
|
|
7
|
+
const pb = Math.abs(p - b);
|
|
8
|
+
const pc = Math.abs(p - c);
|
|
9
|
+
if (pa <= pb && pa <= pc) return a;
|
|
10
|
+
if (pb <= pc) return b;
|
|
11
|
+
return c;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function decodePngToRGBA(
|
|
15
|
+
pngBytes: Uint8Array,
|
|
16
|
+
): { width: number; height: number; pixels: Uint8Array } {
|
|
17
|
+
const sig = [137, 80, 78, 71, 13, 10, 26, 10];
|
|
18
|
+
for (let i = 0; i < 8; i++) {
|
|
19
|
+
if (pngBytes[i] !== sig[i]) throw new Error("Invalid PNG signature");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const view = new DataView(
|
|
23
|
+
pngBytes.buffer,
|
|
24
|
+
pngBytes.byteOffset,
|
|
25
|
+
pngBytes.byteLength,
|
|
26
|
+
);
|
|
27
|
+
let offset = 8;
|
|
28
|
+
|
|
29
|
+
let width = 0;
|
|
30
|
+
let height = 0;
|
|
31
|
+
let bitDepth = 0;
|
|
32
|
+
let colorType = 0;
|
|
33
|
+
const idatChunks: Uint8Array[] = [];
|
|
34
|
+
|
|
35
|
+
while (offset < pngBytes.length) {
|
|
36
|
+
const length = view.getUint32(offset);
|
|
37
|
+
const type = String.fromCharCode(
|
|
38
|
+
pngBytes[offset + 4]!,
|
|
39
|
+
pngBytes[offset + 5]!,
|
|
40
|
+
pngBytes[offset + 6]!,
|
|
41
|
+
pngBytes[offset + 7]!,
|
|
42
|
+
);
|
|
43
|
+
const dataStart = offset + 8;
|
|
44
|
+
|
|
45
|
+
if (type === "IHDR") {
|
|
46
|
+
width = view.getUint32(dataStart);
|
|
47
|
+
height = view.getUint32(dataStart + 4);
|
|
48
|
+
bitDepth = pngBytes[dataStart + 8]!;
|
|
49
|
+
colorType = pngBytes[dataStart + 9]!;
|
|
50
|
+
const interlace = pngBytes[dataStart + 12]!;
|
|
51
|
+
if (bitDepth !== 8) {
|
|
52
|
+
throw new Error(
|
|
53
|
+
`Unsupported bit depth: ${bitDepth} (only 8 supported)`,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
if (colorType !== 2 && colorType !== 6) {
|
|
57
|
+
throw new Error(
|
|
58
|
+
`Unsupported color type: ${colorType} (only RGB=2 and RGBA=6 supported)`,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
if (interlace !== 0) {
|
|
62
|
+
throw new Error("Interlaced PNGs are not supported");
|
|
63
|
+
}
|
|
64
|
+
} else if (type === "IDAT") {
|
|
65
|
+
idatChunks.push(pngBytes.subarray(dataStart, dataStart + length));
|
|
66
|
+
} else if (type === "IEND") {
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
offset = dataStart + length + 4; // +4 for CRC
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (width === 0 || height === 0) throw new Error("Missing IHDR chunk");
|
|
74
|
+
if (idatChunks.length === 0) throw new Error("Missing IDAT chunks");
|
|
75
|
+
|
|
76
|
+
const bpp = colorType === 6 ? 4 : 3;
|
|
77
|
+
const compressed = new Uint8Array(
|
|
78
|
+
idatChunks.reduce((sum, c) => sum + c.length, 0),
|
|
79
|
+
);
|
|
80
|
+
let pos = 0;
|
|
81
|
+
for (const chunk of idatChunks) {
|
|
82
|
+
compressed.set(chunk, pos);
|
|
83
|
+
pos += chunk.length;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const raw = inflateSync(compressed);
|
|
87
|
+
const stride = bpp * width;
|
|
88
|
+
const pixels = new Uint8Array(width * height * 4);
|
|
89
|
+
const prevRow = new Uint8Array(stride);
|
|
90
|
+
const currRow = new Uint8Array(stride);
|
|
91
|
+
|
|
92
|
+
let rawOffset = 0;
|
|
93
|
+
|
|
94
|
+
for (let y = 0; y < height; y++) {
|
|
95
|
+
const filterType = raw[rawOffset++]!;
|
|
96
|
+
|
|
97
|
+
for (let x = 0; x < stride; x++) {
|
|
98
|
+
currRow[x] = raw[rawOffset++]!;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
for (let x = 0; x < stride; x++) {
|
|
102
|
+
const left = x >= bpp ? currRow[x - bpp]! : 0;
|
|
103
|
+
const up = prevRow[x]!;
|
|
104
|
+
const upperLeft = x >= bpp ? prevRow[x - bpp]! : 0;
|
|
105
|
+
|
|
106
|
+
switch (filterType) {
|
|
107
|
+
case 0:
|
|
108
|
+
break;
|
|
109
|
+
case 1:
|
|
110
|
+
currRow[x] = (currRow[x]! + left) & 0xff;
|
|
111
|
+
break;
|
|
112
|
+
case 2:
|
|
113
|
+
currRow[x] = (currRow[x]! + up) & 0xff;
|
|
114
|
+
break;
|
|
115
|
+
case 3:
|
|
116
|
+
currRow[x] = (currRow[x]! + ((left + up) >> 1)) & 0xff;
|
|
117
|
+
break;
|
|
118
|
+
case 4:
|
|
119
|
+
currRow[x] = (currRow[x]! + paethPredictor(left, up, upperLeft)) &
|
|
120
|
+
0xff;
|
|
121
|
+
break;
|
|
122
|
+
default:
|
|
123
|
+
throw new Error(`Unknown filter type: ${filterType}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const rowStart = y * width * 4;
|
|
128
|
+
if (bpp === 4) {
|
|
129
|
+
pixels.set(currRow, rowStart);
|
|
130
|
+
} else {
|
|
131
|
+
for (let x = 0; x < width; x++) {
|
|
132
|
+
pixels[rowStart + x * 4] = currRow[x * 3]!;
|
|
133
|
+
pixels[rowStart + x * 4 + 1] = currRow[x * 3 + 1]!;
|
|
134
|
+
pixels[rowStart + x * 4 + 2] = currRow[x * 3 + 2]!;
|
|
135
|
+
pixels[rowStart + x * 4 + 3] = 255;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
prevRow.set(currRow);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return { width, height, pixels };
|
|
143
|
+
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
export default interface Styler {
|
|
2
2
|
colorEnabled: boolean;
|
|
3
|
+
hyperlinksEnabled: boolean;
|
|
3
4
|
colorText(text: string, colorValue: number): string;
|
|
5
|
+
backgroundColorText(text: string, colorValue: number): string;
|
|
4
6
|
italicText(text: string): string;
|
|
7
|
+
hyperlink(text: string, url: string): string;
|
|
5
8
|
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { ReadStream } from "node:tty";
|
|
2
|
+
import { Buffer } from "node:buffer";
|
|
3
|
+
import type KeyReader from "./KeyReader.ts";
|
|
4
|
+
import { type KeyEvent, SpecialKey } from "./KeyReader.ts";
|
|
5
|
+
|
|
6
|
+
export default class TtyKeyReader implements KeyReader {
|
|
7
|
+
readonly #ttyReadStream: ReadStream;
|
|
8
|
+
|
|
9
|
+
constructor(ttyReadStream: ReadStream) {
|
|
10
|
+
if (ttyReadStream.isTTY === false) {
|
|
11
|
+
throw new Error("The provided ReadStream is not a TTY");
|
|
12
|
+
}
|
|
13
|
+
this.#ttyReadStream = ttyReadStream;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
enableRawMode(): void {
|
|
17
|
+
this.#ttyReadStream.setRawMode(true);
|
|
18
|
+
this.#ttyReadStream.resume();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
disableRawMode(): void {
|
|
22
|
+
this.#ttyReadStream.setRawMode(false);
|
|
23
|
+
this.#ttyReadStream.pause();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
readKey(): Promise<KeyEvent> {
|
|
27
|
+
return new Promise((resolve) => {
|
|
28
|
+
const onData = (data: Buffer): void => {
|
|
29
|
+
this.#ttyReadStream.removeListener("data", onData);
|
|
30
|
+
resolve(TtyKeyReader.#decodeKeyEvent(data));
|
|
31
|
+
};
|
|
32
|
+
this.#ttyReadStream.on("data", onData);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
static #decodeKeyEvent(data: Buffer): KeyEvent {
|
|
37
|
+
if (data.length === 0) {
|
|
38
|
+
return {};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// escape sequences
|
|
42
|
+
if (data[0] === 0x1b) {
|
|
43
|
+
if (data.length === 1) {
|
|
44
|
+
return { specialKey: SpecialKey.ESCAPE };
|
|
45
|
+
}
|
|
46
|
+
if (data.length >= 3 && data[1] === 0x5b) {
|
|
47
|
+
switch (data[2]) {
|
|
48
|
+
case 0x41:
|
|
49
|
+
return { specialKey: SpecialKey.UP };
|
|
50
|
+
case 0x42:
|
|
51
|
+
return { specialKey: SpecialKey.DOWN };
|
|
52
|
+
case 0x43:
|
|
53
|
+
return { specialKey: SpecialKey.RIGHT };
|
|
54
|
+
case 0x44:
|
|
55
|
+
return { specialKey: SpecialKey.LEFT };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return { specialKey: SpecialKey.ESCAPE };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// control characters
|
|
62
|
+
switch (data[0]) {
|
|
63
|
+
case 0x0d:
|
|
64
|
+
return { specialKey: SpecialKey.ENTER };
|
|
65
|
+
case 0x20:
|
|
66
|
+
return { specialKey: SpecialKey.SPACE };
|
|
67
|
+
case 0x7f:
|
|
68
|
+
return { specialKey: SpecialKey.BACKSPACE };
|
|
69
|
+
case 0x09:
|
|
70
|
+
return { specialKey: SpecialKey.TAB };
|
|
71
|
+
case 0x03:
|
|
72
|
+
return { specialKey: SpecialKey.INTERRUPT };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// printable characters
|
|
76
|
+
const str = data.toString("utf8");
|
|
77
|
+
if (str.length > 0) {
|
|
78
|
+
return { key: str };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return {};
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BACKGROUND_COLOR_END,
|
|
3
|
+
FOREGROUND_COLOR_END,
|
|
4
|
+
HYPERLINK_END,
|
|
5
|
+
hyperlinkStart,
|
|
6
|
+
ITALIC_END,
|
|
7
|
+
ITALIC_START,
|
|
8
|
+
} from "./Ansi.ts";
|
|
9
|
+
import type Styler from "./Styler.ts";
|
|
10
|
+
|
|
11
|
+
export default class TtyStyler implements Styler {
|
|
12
|
+
colorEnabled: boolean = true;
|
|
13
|
+
hyperlinksEnabled: boolean;
|
|
14
|
+
|
|
15
|
+
readonly colorLevel: number;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The color level in use.
|
|
19
|
+
*
|
|
20
|
+
* 1 = 16 color
|
|
21
|
+
* 2 = 256 color
|
|
22
|
+
* 3 = 16 million color
|
|
23
|
+
*/
|
|
24
|
+
constructor(colorLevel: number = 1, hyperlinksEnabled: boolean = true) {
|
|
25
|
+
this.colorLevel = colorLevel;
|
|
26
|
+
this.hyperlinksEnabled = hyperlinksEnabled;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
colorText(text: string, colorValue: number): string {
|
|
30
|
+
if (!this.colorEnabled) {
|
|
31
|
+
return text;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (this.colorLevel === 3) {
|
|
35
|
+
return Bun.color(colorValue, "ansi-16m") + text + FOREGROUND_COLOR_END;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (this.colorLevel === 2) {
|
|
39
|
+
return Bun.color(colorValue, "ansi-256") + text + FOREGROUND_COLOR_END;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return Bun.color(colorValue, "ansi-16") + text + FOREGROUND_COLOR_END;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
backgroundColorText(text: string, colorValue: number): string {
|
|
46
|
+
if (!this.colorEnabled) {
|
|
47
|
+
return text;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (this.colorLevel === 3) {
|
|
51
|
+
const r = (colorValue >> 16) & 0xFF;
|
|
52
|
+
const g = (colorValue >> 8) & 0xFF;
|
|
53
|
+
const b = colorValue & 0xFF;
|
|
54
|
+
return `\x1b[48;2;${r};${g};${b}m${text}${BACKGROUND_COLOR_END}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (this.colorLevel === 2) {
|
|
58
|
+
return Bun.color(colorValue, "ansi-256")!.replace("[38;", "[48;") + text +
|
|
59
|
+
BACKGROUND_COLOR_END;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return Bun.color(colorValue, "ansi-16")!.replace(
|
|
63
|
+
/\[(\d+)m/,
|
|
64
|
+
(_, n) => `[${Number(n) + 10}m`,
|
|
65
|
+
) + text + BACKGROUND_COLOR_END;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
italicText(text: string): string {
|
|
69
|
+
return ITALIC_START + text + ITALIC_END;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
hyperlink(text: string, url: string): string {
|
|
73
|
+
if (!this.hyperlinksEnabled) {
|
|
74
|
+
return `${text}: ${url}`;
|
|
75
|
+
}
|
|
76
|
+
return hyperlinkStart(url) + text + HYPERLINK_END;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -57,9 +57,17 @@ export default class TtyTerminal implements Terminal {
|
|
|
57
57
|
|
|
58
58
|
columns(): number {
|
|
59
59
|
try {
|
|
60
|
-
return this.#ttyWriteStream.columns;
|
|
60
|
+
return this.#ttyWriteStream.columns || 80;
|
|
61
61
|
} catch {
|
|
62
62
|
return 80;
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
|
+
|
|
66
|
+
rows(): number {
|
|
67
|
+
try {
|
|
68
|
+
return this.#ttyWriteStream.rows || 24;
|
|
69
|
+
} catch {
|
|
70
|
+
return 24;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
65
73
|
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
import type { WriteStream } from "node:tty";
|
|
3
|
+
|
|
4
|
+
function parseVersion(versionString = ""): {
|
|
5
|
+
major: number;
|
|
6
|
+
minor: number;
|
|
7
|
+
patch: number;
|
|
8
|
+
} {
|
|
9
|
+
if (/^\d{3,4}$/.test(versionString)) {
|
|
10
|
+
const match = /(\d{1,2})(\d{2})/.exec(versionString) ?? [];
|
|
11
|
+
return {
|
|
12
|
+
major: 0,
|
|
13
|
+
minor: Number.parseInt(match[1] ?? "0", 10),
|
|
14
|
+
patch: Number.parseInt(match[2] ?? "0", 10),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const versions = (versionString ?? "").split(".").map((n) =>
|
|
19
|
+
Number.parseInt(n, 10)
|
|
20
|
+
);
|
|
21
|
+
return {
|
|
22
|
+
major: versions[0] ?? 0,
|
|
23
|
+
minor: versions[1] ?? 0,
|
|
24
|
+
patch: versions[2] ?? 0,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export default function supportsHyperlinks(
|
|
29
|
+
stream: WriteStream | undefined,
|
|
30
|
+
): boolean {
|
|
31
|
+
const {
|
|
32
|
+
CI,
|
|
33
|
+
CURSOR_TRACE_ID,
|
|
34
|
+
FORCE_HYPERLINK,
|
|
35
|
+
TEAMCITY_VERSION,
|
|
36
|
+
TERM_PROGRAM,
|
|
37
|
+
TERM_PROGRAM_VERSION,
|
|
38
|
+
VTE_VERSION,
|
|
39
|
+
TERM,
|
|
40
|
+
} = process.env;
|
|
41
|
+
|
|
42
|
+
if (FORCE_HYPERLINK) {
|
|
43
|
+
return !(FORCE_HYPERLINK.length > 0 &&
|
|
44
|
+
Number.parseInt(FORCE_HYPERLINK, 10) === 0);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (stream && !stream.isTTY) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if ("WT_SESSION" in process.env) {
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (CI) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (TEAMCITY_VERSION) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (TERM_PROGRAM) {
|
|
64
|
+
const version = parseVersion(TERM_PROGRAM_VERSION);
|
|
65
|
+
|
|
66
|
+
switch (TERM_PROGRAM) {
|
|
67
|
+
case "iTerm.app": {
|
|
68
|
+
if (version.major === 3) {
|
|
69
|
+
return version.minor >= 1;
|
|
70
|
+
}
|
|
71
|
+
return version.major > 3;
|
|
72
|
+
}
|
|
73
|
+
case "WezTerm": {
|
|
74
|
+
if (/^0-unstable-\d{4}-\d{2}-\d{2}$/.test(TERM_PROGRAM_VERSION!)) {
|
|
75
|
+
const date = TERM_PROGRAM_VERSION!.slice("0-unstable-".length);
|
|
76
|
+
return date >= "2020-06-20";
|
|
77
|
+
}
|
|
78
|
+
return version.major >= 20200620;
|
|
79
|
+
}
|
|
80
|
+
case "vscode": {
|
|
81
|
+
if (CURSOR_TRACE_ID) {
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
return version.major > 1 ||
|
|
85
|
+
(version.major === 1 && version.minor >= 72);
|
|
86
|
+
}
|
|
87
|
+
case "ghostty":
|
|
88
|
+
return true;
|
|
89
|
+
case "zed":
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (VTE_VERSION) {
|
|
95
|
+
if (VTE_VERSION === "0.50.0") {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
const version = parseVersion(VTE_VERSION);
|
|
99
|
+
return version.major > 0 || version.minor >= 50;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
switch (TERM) {
|
|
103
|
+
case "alacritty":
|
|
104
|
+
return true;
|
|
105
|
+
case "xterm-kitty":
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return false;
|
|
110
|
+
}
|
package/src/util/configHelper.ts
CHANGED
|
@@ -9,7 +9,7 @@ function getKeySegment(segment: string, isCustom = false, isArray = false) {
|
|
|
9
9
|
let keySegment = segment;
|
|
10
10
|
if (!isCustom) {
|
|
11
11
|
keySegment = keySegment.replace("-", "_").toUpperCase();
|
|
12
|
-
if (keySegment[0]
|
|
12
|
+
if (keySegment[0]!.match(/[0-9]/)) {
|
|
13
13
|
keySegment = `_${keySegment}`;
|
|
14
14
|
}
|
|
15
15
|
}
|
|
@@ -43,7 +43,7 @@ export function getSubCommandArgumentConfigurationKey(
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
const keySegments: Array<string> = [];
|
|
46
|
-
const firstArgument = argumentAncestry[0]
|
|
46
|
+
const firstArgument = argumentAncestry[0]!;
|
|
47
47
|
if (firstArgument.configurationKey === undefined) {
|
|
48
48
|
keySegments.push(getKeySegment(cliConfig.name));
|
|
49
49
|
keySegments.push(getKeySegment(command.name));
|