@optique/core 1.3.0-dev.2529 → 1.3.0-dev.2532
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/dist/doc.cjs +93 -84
- package/dist/doc.d.cts +12 -1
- package/dist/doc.d.ts +12 -1
- package/dist/doc.js +88 -79
- package/dist/facade.cjs +49 -15
- package/dist/facade.d.cts +12 -1
- package/dist/facade.d.ts +12 -1
- package/dist/facade.js +48 -14
- package/dist/index.cjs +3 -0
- package/dist/index.d.cts +3 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -2
- package/dist/internal/terminal.cjs +7 -0
- package/dist/internal/terminal.d.cts +4 -0
- package/dist/internal/terminal.d.ts +4 -0
- package/dist/internal/terminal.js +5 -0
- package/dist/message-registry.cjs +6 -0
- package/dist/message-registry.d.cts +7 -0
- package/dist/message-registry.d.ts +7 -0
- package/dist/message-registry.js +5 -0
- package/dist/message-renderer.cjs +26 -0
- package/dist/message-renderer.d.cts +19 -0
- package/dist/message-renderer.d.ts +19 -0
- package/dist/message-renderer.js +26 -0
- package/dist/message.cjs +49 -85
- package/dist/message.d.cts +31 -1
- package/dist/message.d.ts +31 -1
- package/dist/message.js +49 -86
- package/dist/terminal-internal.cjs +268 -0
- package/dist/terminal-internal.d.cts +36 -0
- package/dist/terminal-internal.d.ts +36 -0
- package/dist/terminal-internal.js +262 -0
- package/dist/terminal.cjs +73 -0
- package/dist/terminal.d.cts +129 -0
- package/dist/terminal.d.ts +129 -0
- package/dist/terminal.js +72 -0
- package/dist/usage.cjs +110 -134
- package/dist/usage.d.cts +8 -0
- package/dist/usage.d.ts +8 -0
- package/dist/usage.js +110 -134
- package/package.json +18 -2
- package/skills/optique/SKILL.md +3 -3
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { getDisplayWidth } from "./displaywidth.js";
|
|
2
|
+
import { defaultTerminalTheme } from "./terminal.js";
|
|
3
|
+
|
|
4
|
+
//#region src/terminal-internal.ts
|
|
5
|
+
function terminalText(text) {
|
|
6
|
+
return {
|
|
7
|
+
type: "text",
|
|
8
|
+
text
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
function valuesFormatter(theme) {
|
|
12
|
+
if (theme.values != null && theme.values !== defaultTerminalTheme.values) return theme.values;
|
|
13
|
+
if (theme.value != null && theme.value !== defaultTerminalTheme.value) return defaultTerminalTheme.values;
|
|
14
|
+
return (term, context) => ({
|
|
15
|
+
type: "style",
|
|
16
|
+
style: { foreground: "green" },
|
|
17
|
+
children: [defaultTerminalTheme.values(term, context)]
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
function formatTerminalTerm(term, theme = {}, quotes = false, usage) {
|
|
21
|
+
const quote = (s) => quotes ? `\`${s}\`` : s;
|
|
22
|
+
let text;
|
|
23
|
+
switch (term.type) {
|
|
24
|
+
case "text": return terminalText(term.text);
|
|
25
|
+
case "lineBreak": throw new TypeError("Hard breaks must be rendered at the message level.");
|
|
26
|
+
case "optionName":
|
|
27
|
+
text = quote(term.optionName);
|
|
28
|
+
break;
|
|
29
|
+
case "optionNames":
|
|
30
|
+
text = term.optionNames.map(quote).join("/");
|
|
31
|
+
break;
|
|
32
|
+
case "metavar":
|
|
33
|
+
text = quote(term.metavar);
|
|
34
|
+
break;
|
|
35
|
+
case "value":
|
|
36
|
+
text = quotes ? JSON.stringify(term.value) : term.value;
|
|
37
|
+
break;
|
|
38
|
+
case "values":
|
|
39
|
+
text = term.values.map((v) => quotes ? JSON.stringify(v) : v).join(" ");
|
|
40
|
+
break;
|
|
41
|
+
case "envVar":
|
|
42
|
+
text = quote(term.envVar);
|
|
43
|
+
break;
|
|
44
|
+
case "commandLine":
|
|
45
|
+
text = quote(term.commandLine);
|
|
46
|
+
break;
|
|
47
|
+
case "url":
|
|
48
|
+
text = quotes ? `<${term.url.href}>` : term.url.href;
|
|
49
|
+
break;
|
|
50
|
+
case "programName":
|
|
51
|
+
text = term.programName;
|
|
52
|
+
break;
|
|
53
|
+
case "label":
|
|
54
|
+
case "errorLabel":
|
|
55
|
+
text = term.label;
|
|
56
|
+
break;
|
|
57
|
+
case "syntaxPunctuation":
|
|
58
|
+
text = term.punctuation;
|
|
59
|
+
break;
|
|
60
|
+
default: throw new TypeError(`Invalid MessageTerm type: ${term["type"]}.`);
|
|
61
|
+
}
|
|
62
|
+
const ctx = {
|
|
63
|
+
quotes,
|
|
64
|
+
text,
|
|
65
|
+
usage,
|
|
66
|
+
format: (child) => formatTerminalTerm(child, theme, quotes, usage)
|
|
67
|
+
};
|
|
68
|
+
switch (term.type) {
|
|
69
|
+
case "optionName": return (theme.optionName ?? defaultTerminalTheme.optionName)(term, ctx);
|
|
70
|
+
case "optionNames": return (theme.optionNames ?? defaultTerminalTheme.optionNames)(term, ctx);
|
|
71
|
+
case "metavar": return (theme.metavar ?? defaultTerminalTheme.metavar)(term, ctx);
|
|
72
|
+
case "value": return (theme.value ?? defaultTerminalTheme.value)(term, ctx);
|
|
73
|
+
case "values": return valuesFormatter(theme)(term, ctx);
|
|
74
|
+
case "envVar": return (theme.envVar ?? defaultTerminalTheme.envVar)(term, ctx);
|
|
75
|
+
case "commandLine": return (theme.commandLine ?? defaultTerminalTheme.commandLine)(term, ctx);
|
|
76
|
+
case "url": return (theme.url ?? defaultTerminalTheme.url)(term, ctx);
|
|
77
|
+
case "programName": return (theme.programName ?? defaultTerminalTheme.programName)(term, ctx);
|
|
78
|
+
case "label": return (theme.label ?? defaultTerminalTheme.label)(term, ctx);
|
|
79
|
+
case "syntaxPunctuation": return (theme.syntaxPunctuation ?? defaultTerminalTheme.syntaxPunctuation)(term, ctx);
|
|
80
|
+
case "errorLabel": return (theme.errorLabel ?? defaultTerminalTheme.errorLabel)(term, ctx);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const colorNames = [
|
|
84
|
+
"black",
|
|
85
|
+
"red",
|
|
86
|
+
"green",
|
|
87
|
+
"yellow",
|
|
88
|
+
"blue",
|
|
89
|
+
"magenta",
|
|
90
|
+
"cyan",
|
|
91
|
+
"white",
|
|
92
|
+
"brightBlack",
|
|
93
|
+
"brightRed",
|
|
94
|
+
"brightGreen",
|
|
95
|
+
"brightYellow",
|
|
96
|
+
"brightBlue",
|
|
97
|
+
"brightMagenta",
|
|
98
|
+
"brightCyan",
|
|
99
|
+
"brightWhite"
|
|
100
|
+
];
|
|
101
|
+
function colorCode(color, background) {
|
|
102
|
+
const channel = (n) => {
|
|
103
|
+
if (!Number.isInteger(n) || n < 0 || n > 255) throw new RangeError("Terminal color channels must be integers from 0 to 255.");
|
|
104
|
+
return n;
|
|
105
|
+
};
|
|
106
|
+
if (typeof color === "string") {
|
|
107
|
+
const i = colorNames.indexOf(color);
|
|
108
|
+
if (i < 0) throw new RangeError("Unknown terminal color.");
|
|
109
|
+
return String((background ? 40 : 30) + (i >= 8 ? 60 + i - 8 : i));
|
|
110
|
+
}
|
|
111
|
+
const prefix = background ? 48 : 38;
|
|
112
|
+
if ("index" in color) return `${prefix};5;${channel(color.index)}`;
|
|
113
|
+
return `${prefix};2;${color.map(channel).join(";")}`;
|
|
114
|
+
}
|
|
115
|
+
function styleCode(style) {
|
|
116
|
+
const codes = [];
|
|
117
|
+
if (style.bold) codes.push(1);
|
|
118
|
+
if (style.dim) codes.push(2);
|
|
119
|
+
if (style.italic) codes.push(3);
|
|
120
|
+
if (style.underline) codes.push(4);
|
|
121
|
+
if (style.foreground != null) codes.push(colorCode(style.foreground, false));
|
|
122
|
+
if (style.background != null) codes.push(colorCode(style.background, true));
|
|
123
|
+
return codes.length ? `\x1b[${codes.join(";")}m` : "";
|
|
124
|
+
}
|
|
125
|
+
function sameStyle(a, b) {
|
|
126
|
+
return a.bold === b.bold && a.dim === b.dim && a.italic === b.italic && a.underline === b.underline && JSON.stringify(a.foreground) === JSON.stringify(b.foreground) && JSON.stringify(a.background) === JSON.stringify(b.background);
|
|
127
|
+
}
|
|
128
|
+
function* fragmentTokens(fragment, scopes = [], inherited = {}) {
|
|
129
|
+
if (fragment.type === "text") {
|
|
130
|
+
yield {
|
|
131
|
+
text: fragment.text,
|
|
132
|
+
width: getDisplayWidth(fragment.text),
|
|
133
|
+
scopes
|
|
134
|
+
};
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
let next = scopes;
|
|
138
|
+
let style = inherited;
|
|
139
|
+
if (fragment.type === "style") {
|
|
140
|
+
styleCode(fragment.style);
|
|
141
|
+
style = {
|
|
142
|
+
...inherited,
|
|
143
|
+
...fragment.style
|
|
144
|
+
};
|
|
145
|
+
if (!sameStyle(style, inherited)) next = [...scopes, {
|
|
146
|
+
kind: "style",
|
|
147
|
+
style: fragment.style
|
|
148
|
+
}];
|
|
149
|
+
} else if (fragment.type === "link") next = [...scopes, {
|
|
150
|
+
kind: "link",
|
|
151
|
+
href: fragment.href
|
|
152
|
+
}];
|
|
153
|
+
for (const child of fragment.children) yield* fragmentTokens(child, next, style);
|
|
154
|
+
}
|
|
155
|
+
function serializeTokens(tokens, colors = false, resetSuffix = "", ambient = {}) {
|
|
156
|
+
let output = "";
|
|
157
|
+
let active = [];
|
|
158
|
+
function transition(next) {
|
|
159
|
+
let common = 0;
|
|
160
|
+
while (common < active.length && common < next.length && active[common] === next[common]) common++;
|
|
161
|
+
const surviving = active.slice(0, common);
|
|
162
|
+
const restored = surviving.reduce((style, scope) => ({
|
|
163
|
+
...style,
|
|
164
|
+
...scope.style
|
|
165
|
+
}), ambient);
|
|
166
|
+
let reset = false;
|
|
167
|
+
let closedLink = false;
|
|
168
|
+
for (let i = active.length - 1; i >= common; i--) if (active[i].kind === "link") {
|
|
169
|
+
output += `\x1b]8;;\x1b\\${resetSuffix}${styleCode(restored)}`;
|
|
170
|
+
closedLink = true;
|
|
171
|
+
} else reset = true;
|
|
172
|
+
active = surviving;
|
|
173
|
+
if (closedLink) {
|
|
174
|
+
const parentLink = active.findLast((scope) => scope.kind === "link");
|
|
175
|
+
if (parentLink != null) output += `\x1b]8;;${parentLink.href}\x1b\\`;
|
|
176
|
+
}
|
|
177
|
+
if (reset) output += "\x1B[0m" + resetSuffix + styleCode(restored);
|
|
178
|
+
for (const scope of next.slice(common)) {
|
|
179
|
+
if (scope.kind === "link") output += `\x1b]8;;${scope.href}\x1b\\`;
|
|
180
|
+
else if (scope.style) if (Object.values(scope.style).includes(false)) {
|
|
181
|
+
const inherited = active.reduce((s, x) => ({
|
|
182
|
+
...s,
|
|
183
|
+
...x.style
|
|
184
|
+
}), ambient);
|
|
185
|
+
output += "\x1B[0m" + resetSuffix + styleCode({
|
|
186
|
+
...inherited,
|
|
187
|
+
...scope.style
|
|
188
|
+
});
|
|
189
|
+
} else output += styleCode(scope.style);
|
|
190
|
+
active = [...active, scope];
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
const items = [...tokens];
|
|
194
|
+
for (let i = 0; i < items.length; i++) {
|
|
195
|
+
const token = items[i];
|
|
196
|
+
if (colors && token.width === -1) {
|
|
197
|
+
const next = items.slice(i + 1).find((t) => t.width !== -1)?.scopes ?? [];
|
|
198
|
+
let common = 0;
|
|
199
|
+
while (common < active.length && common < next.length && active[common] === next[common]) common++;
|
|
200
|
+
transition(active.slice(0, common));
|
|
201
|
+
} else if (colors) transition(token.scopes);
|
|
202
|
+
output += token.text;
|
|
203
|
+
}
|
|
204
|
+
if (colors) transition([]);
|
|
205
|
+
return output;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Serializes one semantic leaf for a terminal surface outside a message.
|
|
209
|
+
* @param term The semantic leaf to render.
|
|
210
|
+
* @param theme Optional overrides for the default terminal theme.
|
|
211
|
+
* @param colors Whether to emit styles and hyperlinks.
|
|
212
|
+
* @param usage The leaf's usage context, when applicable.
|
|
213
|
+
* @param ambient The style surrounding the rendered leaf.
|
|
214
|
+
* @returns The leaf's styled or plain representation.
|
|
215
|
+
* @throws {TypeError} If the term is a hard break or has an unknown type.
|
|
216
|
+
* @throws {RangeError} If a color channel or palette index is invalid.
|
|
217
|
+
* @since 1.3.0
|
|
218
|
+
* @internal
|
|
219
|
+
*/
|
|
220
|
+
function renderTerminalTerm(term, theme, colors, usage, ambient) {
|
|
221
|
+
return serializeTokens(fragmentTokens(formatTerminalTerm(term, theme, false, usage)), colors, "", ambient);
|
|
222
|
+
}
|
|
223
|
+
/** Memoizes semantic leaves for one layout pass, including measurement. */
|
|
224
|
+
function cacheTerminalTheme(theme = {}) {
|
|
225
|
+
function cache(formatter) {
|
|
226
|
+
const fragments = /* @__PURE__ */ new Map();
|
|
227
|
+
return (term, context) => {
|
|
228
|
+
const key = JSON.stringify([
|
|
229
|
+
term,
|
|
230
|
+
context.quotes,
|
|
231
|
+
context.usage
|
|
232
|
+
]);
|
|
233
|
+
let fragment = fragments.get(key);
|
|
234
|
+
if (fragment == null) {
|
|
235
|
+
fragment = formatter(term, context);
|
|
236
|
+
fragments.set(key, fragment);
|
|
237
|
+
}
|
|
238
|
+
return fragment;
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
return {
|
|
242
|
+
optionName: cache(theme.optionName ?? defaultTerminalTheme.optionName),
|
|
243
|
+
optionNames: cache(theme.optionNames ?? defaultTerminalTheme.optionNames),
|
|
244
|
+
metavar: cache(theme.metavar ?? defaultTerminalTheme.metavar),
|
|
245
|
+
value: cache(theme.value ?? defaultTerminalTheme.value),
|
|
246
|
+
values: cache(valuesFormatter(theme)),
|
|
247
|
+
envVar: cache(theme.envVar ?? defaultTerminalTheme.envVar),
|
|
248
|
+
commandLine: cache(theme.commandLine ?? defaultTerminalTheme.commandLine),
|
|
249
|
+
url: cache(theme.url ?? defaultTerminalTheme.url),
|
|
250
|
+
programName: cache(theme.programName ?? defaultTerminalTheme.programName),
|
|
251
|
+
label: cache(theme.label ?? defaultTerminalTheme.label),
|
|
252
|
+
syntaxPunctuation: cache(theme.syntaxPunctuation ?? defaultTerminalTheme.syntaxPunctuation),
|
|
253
|
+
errorLabel: cache(theme.errorLabel ?? defaultTerminalTheme.errorLabel),
|
|
254
|
+
annotationStyles: theme.annotationStyles == null ? void 0 : {
|
|
255
|
+
default: theme.annotationStyles.default == null ? void 0 : { ...theme.annotationStyles.default },
|
|
256
|
+
choices: theme.annotationStyles.choices == null ? void 0 : { ...theme.annotationStyles.choices }
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
//#endregion
|
|
262
|
+
export { cacheTerminalTheme, formatTerminalTerm, fragmentTokens, renderTerminalTerm, serializeTokens, styleCode, terminalText };
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
|
|
2
|
+
//#region src/terminal.ts
|
|
3
|
+
const plain = (text) => ({
|
|
4
|
+
type: "text",
|
|
5
|
+
text
|
|
6
|
+
});
|
|
7
|
+
const styled = (text, style) => ({
|
|
8
|
+
type: "style",
|
|
9
|
+
style,
|
|
10
|
+
children: [plain(text)]
|
|
11
|
+
});
|
|
12
|
+
/**
|
|
13
|
+
* Optique's built-in terminal theme, with every formatter available for delegation.
|
|
14
|
+
* @since 1.3.0
|
|
15
|
+
*/
|
|
16
|
+
const defaultTerminalTheme = {
|
|
17
|
+
optionName: (_term, ctx) => styled(ctx.text, ctx.usage === "command" ? { bold: true } : { italic: true }),
|
|
18
|
+
optionNames: (term, ctx) => ({
|
|
19
|
+
type: "concat",
|
|
20
|
+
children: term.optionNames.flatMap((name, i) => [...i === 0 ? [] : [ctx.format({
|
|
21
|
+
type: "syntaxPunctuation",
|
|
22
|
+
punctuation: "/",
|
|
23
|
+
kind: "messageOptionSeparator"
|
|
24
|
+
})], ctx.format({
|
|
25
|
+
type: "optionName",
|
|
26
|
+
optionName: name
|
|
27
|
+
})])
|
|
28
|
+
}),
|
|
29
|
+
metavar: (_term, ctx) => ctx.usage === "optionValue" ? {
|
|
30
|
+
type: "style",
|
|
31
|
+
style: { underline: true },
|
|
32
|
+
children: [styled(ctx.text, { dim: true })]
|
|
33
|
+
} : styled(ctx.text, ctx.usage === "argument" ? { underline: true } : { bold: true }),
|
|
34
|
+
value: (_term, ctx) => ctx.usage === "literal" ? plain(ctx.text) : styled(ctx.text, { foreground: "green" }),
|
|
35
|
+
values: (term, ctx) => ({
|
|
36
|
+
type: "concat",
|
|
37
|
+
children: term.values.flatMap((value, i) => [...i === 0 ? [] : [plain(" ")], ctx.format({
|
|
38
|
+
type: "value",
|
|
39
|
+
value
|
|
40
|
+
})])
|
|
41
|
+
}),
|
|
42
|
+
envVar: (_term, ctx) => styled(ctx.text, {
|
|
43
|
+
bold: true,
|
|
44
|
+
underline: true
|
|
45
|
+
}),
|
|
46
|
+
commandLine: (_term, ctx) => styled(ctx.text, { foreground: "cyan" }),
|
|
47
|
+
url: (term, ctx) => ({
|
|
48
|
+
type: "link",
|
|
49
|
+
href: term.url.href,
|
|
50
|
+
children: [plain(ctx.text)]
|
|
51
|
+
}),
|
|
52
|
+
programName: (_term, ctx) => styled(ctx.text, { bold: true }),
|
|
53
|
+
label: (term, ctx) => term.kind === "usageSummary" || term.kind === "choices" ? plain(ctx.text) : styled(ctx.text, {
|
|
54
|
+
bold: true,
|
|
55
|
+
dim: true
|
|
56
|
+
}),
|
|
57
|
+
syntaxPunctuation: (term, ctx) => [
|
|
58
|
+
"alternative",
|
|
59
|
+
"messageOptionSeparator",
|
|
60
|
+
"defaultPrefix",
|
|
61
|
+
"defaultSuffix",
|
|
62
|
+
"choicesPrefix",
|
|
63
|
+
"choicesSuffix"
|
|
64
|
+
].includes(term.kind) ? plain(ctx.text) : styled(ctx.text, { dim: true }),
|
|
65
|
+
errorLabel: (_term, ctx) => plain(ctx.text),
|
|
66
|
+
annotationStyles: {
|
|
67
|
+
default: { dim: true },
|
|
68
|
+
choices: { dim: true }
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
//#endregion
|
|
73
|
+
exports.defaultTerminalTheme = defaultTerminalTheme;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { MessageTerm } from "./message.cjs";
|
|
2
|
+
|
|
3
|
+
//#region src/terminal.d.ts
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A program's executable name.
|
|
7
|
+
* @since 1.3.0
|
|
8
|
+
*/
|
|
9
|
+
interface ProgramNameTerm {
|
|
10
|
+
readonly type: "programName";
|
|
11
|
+
readonly programName: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* A heading or annotation label.
|
|
15
|
+
* @since 1.3.0
|
|
16
|
+
*/
|
|
17
|
+
interface LabelTerm {
|
|
18
|
+
readonly type: "label";
|
|
19
|
+
readonly label: string;
|
|
20
|
+
readonly kind: "usage" | "usageSummary" | "section" | "examples" | "author" | "bugs" | "choices";
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Punctuation supplied by a terminal renderer.
|
|
24
|
+
* @since 1.3.0
|
|
25
|
+
*/
|
|
26
|
+
interface SyntaxPunctuationTerm {
|
|
27
|
+
readonly type: "syntaxPunctuation";
|
|
28
|
+
readonly punctuation: string;
|
|
29
|
+
readonly kind: "optionalOpen" | "optionalClose" | "groupOpen" | "groupClose" | "alternative" | "ellipsis" | "passthrough" | "optionSeparator" | "messageOptionSeparator" | "defaultPrefix" | "defaultSuffix" | "choicesPrefix" | "choicesSuffix";
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The label preceding an error message.
|
|
33
|
+
* @since 1.3.0
|
|
34
|
+
*/
|
|
35
|
+
interface ErrorLabelTerm {
|
|
36
|
+
readonly type: "errorLabel";
|
|
37
|
+
readonly label: string;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Semantic leaves shared by message, usage, and help renderers.
|
|
41
|
+
* @since 1.3.0
|
|
42
|
+
*/
|
|
43
|
+
type TerminalTerm = MessageTerm | ProgramNameTerm | LabelTerm | SyntaxPunctuationTerm | ErrorLabelTerm;
|
|
44
|
+
/**
|
|
45
|
+
* A terminal color. RGB channels and palette indices are integers from 0 to 255.
|
|
46
|
+
* @since 1.3.0
|
|
47
|
+
*/
|
|
48
|
+
type TerminalColor = "black" | "red" | "green" | "yellow" | "blue" | "magenta" | "cyan" | "white" | "brightBlack" | "brightRed" | "brightGreen" | "brightYellow" | "brightBlue" | "brightMagenta" | "brightCyan" | "brightWhite" | readonly [number, number, number] | {
|
|
49
|
+
readonly index: number;
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Inherited terminal styling; false disables an inherited attribute.
|
|
53
|
+
* @since 1.3.0
|
|
54
|
+
*/
|
|
55
|
+
interface TerminalStyle {
|
|
56
|
+
readonly foreground?: TerminalColor;
|
|
57
|
+
readonly background?: TerminalColor;
|
|
58
|
+
readonly bold?: boolean;
|
|
59
|
+
readonly dim?: boolean;
|
|
60
|
+
readonly italic?: boolean;
|
|
61
|
+
readonly underline?: boolean;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Structured output. Each text leaf is an indivisible wrapping unit.
|
|
65
|
+
* @since 1.3.0
|
|
66
|
+
*/
|
|
67
|
+
type TerminalFragment = {
|
|
68
|
+
readonly type: "text";
|
|
69
|
+
readonly text: string;
|
|
70
|
+
} | {
|
|
71
|
+
readonly type: "concat";
|
|
72
|
+
readonly children: readonly TerminalFragment[];
|
|
73
|
+
} | {
|
|
74
|
+
readonly type: "style";
|
|
75
|
+
readonly style: TerminalStyle;
|
|
76
|
+
readonly children: readonly TerminalFragment[];
|
|
77
|
+
} | {
|
|
78
|
+
readonly type: "link";
|
|
79
|
+
readonly href: string;
|
|
80
|
+
readonly children: readonly TerminalFragment[];
|
|
81
|
+
};
|
|
82
|
+
/**
|
|
83
|
+
* Context supplied to a semantic formatter.
|
|
84
|
+
* @since 1.3.0
|
|
85
|
+
*/
|
|
86
|
+
interface TerminalFormatContext {
|
|
87
|
+
/** Whether the default display text includes quotes. */
|
|
88
|
+
readonly quotes: boolean;
|
|
89
|
+
/** Default display text, quoted but not styled. */
|
|
90
|
+
readonly text: string;
|
|
91
|
+
/** The purpose of a leaf originating in a usage description. */
|
|
92
|
+
readonly usage?: "argument" | "optionValue" | "command" | "literal";
|
|
93
|
+
/** Formats a child through the effective theme. Same-role recursion is the author's responsibility. */
|
|
94
|
+
readonly format: (term: Exclude<TerminalTerm, {
|
|
95
|
+
readonly type: "lineBreak";
|
|
96
|
+
}>) => TerminalFragment;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Formats a semantic leaf without emitting ANSI.
|
|
100
|
+
* @since 1.3.0
|
|
101
|
+
*/
|
|
102
|
+
type TerminalFormatter<T extends TerminalTerm = TerminalTerm> = (term: T, context: TerminalFormatContext) => TerminalFragment;
|
|
103
|
+
type Role = Exclude<TerminalTerm["type"], "text" | "lineBreak">;
|
|
104
|
+
type Formatters = { readonly [K in Role]: TerminalFormatter<Extract<TerminalTerm, {
|
|
105
|
+
readonly type: K;
|
|
106
|
+
}>> };
|
|
107
|
+
/**
|
|
108
|
+
* A partial theme; omitted roles retain their defaults.
|
|
109
|
+
* @since 1.3.0
|
|
110
|
+
*/
|
|
111
|
+
type TerminalTheme = Partial<Formatters> & {
|
|
112
|
+
/** Surrounding annotation styles. Missing styles are dim; {} disables dim. */
|
|
113
|
+
readonly annotationStyles?: {
|
|
114
|
+
readonly default?: TerminalStyle;
|
|
115
|
+
readonly choices?: TerminalStyle;
|
|
116
|
+
};
|
|
117
|
+
};
|
|
118
|
+
/**
|
|
119
|
+
* Optique's built-in terminal theme, with every formatter available for delegation.
|
|
120
|
+
* @since 1.3.0
|
|
121
|
+
*/
|
|
122
|
+
declare const defaultTerminalTheme: Formatters & {
|
|
123
|
+
readonly annotationStyles: {
|
|
124
|
+
readonly default: TerminalStyle;
|
|
125
|
+
readonly choices: TerminalStyle;
|
|
126
|
+
};
|
|
127
|
+
};
|
|
128
|
+
//#endregion
|
|
129
|
+
export { ErrorLabelTerm, LabelTerm, ProgramNameTerm, SyntaxPunctuationTerm, TerminalColor, TerminalFormatContext, TerminalFormatter, TerminalFragment, TerminalStyle, TerminalTerm, TerminalTheme, defaultTerminalTheme };
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { MessageTerm } from "./message.js";
|
|
2
|
+
|
|
3
|
+
//#region src/terminal.d.ts
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A program's executable name.
|
|
7
|
+
* @since 1.3.0
|
|
8
|
+
*/
|
|
9
|
+
interface ProgramNameTerm {
|
|
10
|
+
readonly type: "programName";
|
|
11
|
+
readonly programName: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* A heading or annotation label.
|
|
15
|
+
* @since 1.3.0
|
|
16
|
+
*/
|
|
17
|
+
interface LabelTerm {
|
|
18
|
+
readonly type: "label";
|
|
19
|
+
readonly label: string;
|
|
20
|
+
readonly kind: "usage" | "usageSummary" | "section" | "examples" | "author" | "bugs" | "choices";
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Punctuation supplied by a terminal renderer.
|
|
24
|
+
* @since 1.3.0
|
|
25
|
+
*/
|
|
26
|
+
interface SyntaxPunctuationTerm {
|
|
27
|
+
readonly type: "syntaxPunctuation";
|
|
28
|
+
readonly punctuation: string;
|
|
29
|
+
readonly kind: "optionalOpen" | "optionalClose" | "groupOpen" | "groupClose" | "alternative" | "ellipsis" | "passthrough" | "optionSeparator" | "messageOptionSeparator" | "defaultPrefix" | "defaultSuffix" | "choicesPrefix" | "choicesSuffix";
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The label preceding an error message.
|
|
33
|
+
* @since 1.3.0
|
|
34
|
+
*/
|
|
35
|
+
interface ErrorLabelTerm {
|
|
36
|
+
readonly type: "errorLabel";
|
|
37
|
+
readonly label: string;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Semantic leaves shared by message, usage, and help renderers.
|
|
41
|
+
* @since 1.3.0
|
|
42
|
+
*/
|
|
43
|
+
type TerminalTerm = MessageTerm | ProgramNameTerm | LabelTerm | SyntaxPunctuationTerm | ErrorLabelTerm;
|
|
44
|
+
/**
|
|
45
|
+
* A terminal color. RGB channels and palette indices are integers from 0 to 255.
|
|
46
|
+
* @since 1.3.0
|
|
47
|
+
*/
|
|
48
|
+
type TerminalColor = "black" | "red" | "green" | "yellow" | "blue" | "magenta" | "cyan" | "white" | "brightBlack" | "brightRed" | "brightGreen" | "brightYellow" | "brightBlue" | "brightMagenta" | "brightCyan" | "brightWhite" | readonly [number, number, number] | {
|
|
49
|
+
readonly index: number;
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Inherited terminal styling; false disables an inherited attribute.
|
|
53
|
+
* @since 1.3.0
|
|
54
|
+
*/
|
|
55
|
+
interface TerminalStyle {
|
|
56
|
+
readonly foreground?: TerminalColor;
|
|
57
|
+
readonly background?: TerminalColor;
|
|
58
|
+
readonly bold?: boolean;
|
|
59
|
+
readonly dim?: boolean;
|
|
60
|
+
readonly italic?: boolean;
|
|
61
|
+
readonly underline?: boolean;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Structured output. Each text leaf is an indivisible wrapping unit.
|
|
65
|
+
* @since 1.3.0
|
|
66
|
+
*/
|
|
67
|
+
type TerminalFragment = {
|
|
68
|
+
readonly type: "text";
|
|
69
|
+
readonly text: string;
|
|
70
|
+
} | {
|
|
71
|
+
readonly type: "concat";
|
|
72
|
+
readonly children: readonly TerminalFragment[];
|
|
73
|
+
} | {
|
|
74
|
+
readonly type: "style";
|
|
75
|
+
readonly style: TerminalStyle;
|
|
76
|
+
readonly children: readonly TerminalFragment[];
|
|
77
|
+
} | {
|
|
78
|
+
readonly type: "link";
|
|
79
|
+
readonly href: string;
|
|
80
|
+
readonly children: readonly TerminalFragment[];
|
|
81
|
+
};
|
|
82
|
+
/**
|
|
83
|
+
* Context supplied to a semantic formatter.
|
|
84
|
+
* @since 1.3.0
|
|
85
|
+
*/
|
|
86
|
+
interface TerminalFormatContext {
|
|
87
|
+
/** Whether the default display text includes quotes. */
|
|
88
|
+
readonly quotes: boolean;
|
|
89
|
+
/** Default display text, quoted but not styled. */
|
|
90
|
+
readonly text: string;
|
|
91
|
+
/** The purpose of a leaf originating in a usage description. */
|
|
92
|
+
readonly usage?: "argument" | "optionValue" | "command" | "literal";
|
|
93
|
+
/** Formats a child through the effective theme. Same-role recursion is the author's responsibility. */
|
|
94
|
+
readonly format: (term: Exclude<TerminalTerm, {
|
|
95
|
+
readonly type: "lineBreak";
|
|
96
|
+
}>) => TerminalFragment;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Formats a semantic leaf without emitting ANSI.
|
|
100
|
+
* @since 1.3.0
|
|
101
|
+
*/
|
|
102
|
+
type TerminalFormatter<T extends TerminalTerm = TerminalTerm> = (term: T, context: TerminalFormatContext) => TerminalFragment;
|
|
103
|
+
type Role = Exclude<TerminalTerm["type"], "text" | "lineBreak">;
|
|
104
|
+
type Formatters = { readonly [K in Role]: TerminalFormatter<Extract<TerminalTerm, {
|
|
105
|
+
readonly type: K;
|
|
106
|
+
}>> };
|
|
107
|
+
/**
|
|
108
|
+
* A partial theme; omitted roles retain their defaults.
|
|
109
|
+
* @since 1.3.0
|
|
110
|
+
*/
|
|
111
|
+
type TerminalTheme = Partial<Formatters> & {
|
|
112
|
+
/** Surrounding annotation styles. Missing styles are dim; {} disables dim. */
|
|
113
|
+
readonly annotationStyles?: {
|
|
114
|
+
readonly default?: TerminalStyle;
|
|
115
|
+
readonly choices?: TerminalStyle;
|
|
116
|
+
};
|
|
117
|
+
};
|
|
118
|
+
/**
|
|
119
|
+
* Optique's built-in terminal theme, with every formatter available for delegation.
|
|
120
|
+
* @since 1.3.0
|
|
121
|
+
*/
|
|
122
|
+
declare const defaultTerminalTheme: Formatters & {
|
|
123
|
+
readonly annotationStyles: {
|
|
124
|
+
readonly default: TerminalStyle;
|
|
125
|
+
readonly choices: TerminalStyle;
|
|
126
|
+
};
|
|
127
|
+
};
|
|
128
|
+
//#endregion
|
|
129
|
+
export { ErrorLabelTerm, LabelTerm, ProgramNameTerm, SyntaxPunctuationTerm, TerminalColor, TerminalFormatContext, TerminalFormatter, TerminalFragment, TerminalStyle, TerminalTerm, TerminalTheme, defaultTerminalTheme };
|
package/dist/terminal.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
//#region src/terminal.ts
|
|
2
|
+
const plain = (text) => ({
|
|
3
|
+
type: "text",
|
|
4
|
+
text
|
|
5
|
+
});
|
|
6
|
+
const styled = (text, style) => ({
|
|
7
|
+
type: "style",
|
|
8
|
+
style,
|
|
9
|
+
children: [plain(text)]
|
|
10
|
+
});
|
|
11
|
+
/**
|
|
12
|
+
* Optique's built-in terminal theme, with every formatter available for delegation.
|
|
13
|
+
* @since 1.3.0
|
|
14
|
+
*/
|
|
15
|
+
const defaultTerminalTheme = {
|
|
16
|
+
optionName: (_term, ctx) => styled(ctx.text, ctx.usage === "command" ? { bold: true } : { italic: true }),
|
|
17
|
+
optionNames: (term, ctx) => ({
|
|
18
|
+
type: "concat",
|
|
19
|
+
children: term.optionNames.flatMap((name, i) => [...i === 0 ? [] : [ctx.format({
|
|
20
|
+
type: "syntaxPunctuation",
|
|
21
|
+
punctuation: "/",
|
|
22
|
+
kind: "messageOptionSeparator"
|
|
23
|
+
})], ctx.format({
|
|
24
|
+
type: "optionName",
|
|
25
|
+
optionName: name
|
|
26
|
+
})])
|
|
27
|
+
}),
|
|
28
|
+
metavar: (_term, ctx) => ctx.usage === "optionValue" ? {
|
|
29
|
+
type: "style",
|
|
30
|
+
style: { underline: true },
|
|
31
|
+
children: [styled(ctx.text, { dim: true })]
|
|
32
|
+
} : styled(ctx.text, ctx.usage === "argument" ? { underline: true } : { bold: true }),
|
|
33
|
+
value: (_term, ctx) => ctx.usage === "literal" ? plain(ctx.text) : styled(ctx.text, { foreground: "green" }),
|
|
34
|
+
values: (term, ctx) => ({
|
|
35
|
+
type: "concat",
|
|
36
|
+
children: term.values.flatMap((value, i) => [...i === 0 ? [] : [plain(" ")], ctx.format({
|
|
37
|
+
type: "value",
|
|
38
|
+
value
|
|
39
|
+
})])
|
|
40
|
+
}),
|
|
41
|
+
envVar: (_term, ctx) => styled(ctx.text, {
|
|
42
|
+
bold: true,
|
|
43
|
+
underline: true
|
|
44
|
+
}),
|
|
45
|
+
commandLine: (_term, ctx) => styled(ctx.text, { foreground: "cyan" }),
|
|
46
|
+
url: (term, ctx) => ({
|
|
47
|
+
type: "link",
|
|
48
|
+
href: term.url.href,
|
|
49
|
+
children: [plain(ctx.text)]
|
|
50
|
+
}),
|
|
51
|
+
programName: (_term, ctx) => styled(ctx.text, { bold: true }),
|
|
52
|
+
label: (term, ctx) => term.kind === "usageSummary" || term.kind === "choices" ? plain(ctx.text) : styled(ctx.text, {
|
|
53
|
+
bold: true,
|
|
54
|
+
dim: true
|
|
55
|
+
}),
|
|
56
|
+
syntaxPunctuation: (term, ctx) => [
|
|
57
|
+
"alternative",
|
|
58
|
+
"messageOptionSeparator",
|
|
59
|
+
"defaultPrefix",
|
|
60
|
+
"defaultSuffix",
|
|
61
|
+
"choicesPrefix",
|
|
62
|
+
"choicesSuffix"
|
|
63
|
+
].includes(term.kind) ? plain(ctx.text) : styled(ctx.text, { dim: true }),
|
|
64
|
+
errorLabel: (_term, ctx) => plain(ctx.text),
|
|
65
|
+
annotationStyles: {
|
|
66
|
+
default: { dim: true },
|
|
67
|
+
choices: { dim: true }
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
//#endregion
|
|
72
|
+
export { defaultTerminalTheme };
|