@rind-ai/cli 0.4.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/rind.js +5 -5
- package/lib/assistant-renderer.js +179 -265
- package/lib/choice-menu-state.js +46 -46
- package/lib/cli-input-actions.js +548 -0
- package/lib/cli-output-controller.js +460 -0
- package/lib/cli-runtime-controller.js +350 -0
- package/lib/cli-state-store.js +32 -0
- package/lib/cli-state.js +41 -0
- package/lib/command-controller.js +159 -126
- package/lib/compact-context-state.js +22 -22
- package/lib/components/assistant-message.js +169 -0
- package/lib/components/composer-area.js +25 -0
- package/lib/components/dynamic-block.js +20 -0
- package/lib/components/monitor-stack.js +35 -0
- package/lib/components/text-block.js +47 -0
- package/lib/components/tool-block.js +122 -0
- package/lib/composer-terminal.js +224 -203
- package/lib/event-controller.js +243 -242
- package/lib/frontend-cli-implementation.js +656 -1111
- package/lib/input-controller.js +75 -94
- package/lib/input-errors.js +3 -3
- package/lib/interrupt-state.js +9 -9
- package/lib/line-editor.js +541 -541
- package/lib/local-slash-commands.js +217 -0
- package/lib/markdown-lines.js +103 -0
- package/lib/model-menu-state.js +50 -50
- package/lib/one-shot-progress.js +145 -0
- package/lib/one-shot.js +228 -0
- package/lib/question-menu-state.js +61 -0
- package/lib/rendering.js +1309 -1060
- package/lib/runtime-client.js +241 -193
- package/lib/runtime-env.js +21 -21
- package/lib/runtime-protocol.js +122 -15
- package/lib/slash-command-mode.js +0 -11
- package/lib/slash-menu-state.js +59 -59
- package/lib/{background-controller.js → task-monitor-controller.js} +411 -289
- package/lib/terminal-key.js +97 -97
- package/lib/text-width.js +335 -151
- package/lib/theme-menu-state.js +31 -0
- package/lib/theme.js +134 -0
- package/lib/tool-display.js +675 -0
- package/lib/tui/component.js +55 -0
- package/lib/tui/cursor.js +29 -0
- package/lib/tui/input-buffer.js +172 -0
- package/lib/tui/tui.js +591 -0
- package/lib/turn-controller.js +68 -78
- package/package.json +28 -28
- package/lib/assistant-stream-buffer.js +0 -25
- package/lib/terminal-ui.js +0 -581
package/lib/text-width.js
CHANGED
|
@@ -1,151 +1,335 @@
|
|
|
1
|
-
const ANSI_RE = /\x1b\[[0-?]*[ -/]*[@-~]/g;
|
|
2
|
-
const
|
|
3
|
-
|
|
4
|
-
:
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
let
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
let
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
startColumn,
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
1
|
+
const ANSI_RE = /\x1b\[[0-?]*[ -/]*[@-~]/g;
|
|
2
|
+
const LINE_BREAK_RE = /\r\n|\r|\n/;
|
|
3
|
+
const segmenter = typeof Intl?.Segmenter === "function"
|
|
4
|
+
? new Intl.Segmenter(undefined, { granularity: "grapheme" })
|
|
5
|
+
: null;
|
|
6
|
+
|
|
7
|
+
export function stripAnsi(value) {
|
|
8
|
+
return String(value || "").replace(ANSI_RE, "");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function graphemes(value) {
|
|
12
|
+
const text = String(value || "");
|
|
13
|
+
return segmenter
|
|
14
|
+
? Array.from(segmenter.segment(text), (segment) => segment.segment)
|
|
15
|
+
: Array.from(text);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function textWidth(value) {
|
|
19
|
+
return graphemes(stripAnsi(value)).reduce((width, segment) => width + segmentWidth(segment), 0);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function wrapTextCells(value, firstWidth, continuationWidth = firstWidth) {
|
|
23
|
+
const chars = graphemes(value);
|
|
24
|
+
if (!chars.length) {
|
|
25
|
+
return [{
|
|
26
|
+
text: "",
|
|
27
|
+
startColumn: 0,
|
|
28
|
+
length: 0,
|
|
29
|
+
allowsEnd: true,
|
|
30
|
+
}];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const chunks = [];
|
|
34
|
+
let startColumn = 0;
|
|
35
|
+
let availableWidth = positiveWidth(firstWidth);
|
|
36
|
+
const nextLineWidth = positiveWidth(continuationWidth);
|
|
37
|
+
while (startColumn < chars.length) {
|
|
38
|
+
let endColumn = startColumn;
|
|
39
|
+
let width = 0;
|
|
40
|
+
while (endColumn < chars.length) {
|
|
41
|
+
const nextWidth = segmentWidth(chars[endColumn]);
|
|
42
|
+
if (width > 0 && width + nextWidth > availableWidth) {
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
width += nextWidth;
|
|
46
|
+
endColumn += 1;
|
|
47
|
+
}
|
|
48
|
+
chunks.push({
|
|
49
|
+
text: chars.slice(startColumn, endColumn).join(""),
|
|
50
|
+
startColumn,
|
|
51
|
+
length: endColumn - startColumn,
|
|
52
|
+
allowsEnd: width < availableWidth,
|
|
53
|
+
});
|
|
54
|
+
startColumn = endColumn;
|
|
55
|
+
availableWidth = nextLineWidth;
|
|
56
|
+
}
|
|
57
|
+
return chunks;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function clipCells(value, maxWidth) {
|
|
61
|
+
const text = String(value || "");
|
|
62
|
+
if (textWidth(text) <= maxWidth) {
|
|
63
|
+
return text;
|
|
64
|
+
}
|
|
65
|
+
const suffix = "...";
|
|
66
|
+
const suffixWidth = textWidth(suffix);
|
|
67
|
+
if (maxWidth <= suffixWidth) {
|
|
68
|
+
return suffix.slice(0, Math.max(0, maxWidth));
|
|
69
|
+
}
|
|
70
|
+
return `${takeStartCells(text, maxWidth - suffixWidth)}${suffix}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function truncateToWidth(value, maxWidth, ellipsis = "...") {
|
|
74
|
+
const text = String(value || "");
|
|
75
|
+
if (textWidth(text) <= maxWidth) {
|
|
76
|
+
return text;
|
|
77
|
+
}
|
|
78
|
+
const suffixWidth = textWidth(ellipsis);
|
|
79
|
+
if (maxWidth <= suffixWidth) {
|
|
80
|
+
return ellipsis.slice(0, Math.max(0, maxWidth));
|
|
81
|
+
}
|
|
82
|
+
return `${takeStartCells(text, maxWidth - suffixWidth)}${ellipsis}`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function expandTabs(value, tabWidth = 4) {
|
|
86
|
+
const text = String(value || "");
|
|
87
|
+
if (!text.includes("\t")) {
|
|
88
|
+
return text;
|
|
89
|
+
}
|
|
90
|
+
let column = 0;
|
|
91
|
+
let output = "";
|
|
92
|
+
for (const segment of graphemes(text)) {
|
|
93
|
+
if (segment === "\t") {
|
|
94
|
+
const spaces = tabWidth - (column % tabWidth);
|
|
95
|
+
output += " ".repeat(spaces);
|
|
96
|
+
column += spaces;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
output += segment;
|
|
100
|
+
column += segmentWidth(segment);
|
|
101
|
+
}
|
|
102
|
+
return output;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function wrapTextWithAnsi(value, firstWidth, continuationWidth = firstWidth) {
|
|
106
|
+
const source = String(value ?? "");
|
|
107
|
+
const first = positiveWidth(firstWidth);
|
|
108
|
+
const cont = positiveWidth(continuationWidth);
|
|
109
|
+
const lines = [];
|
|
110
|
+
for (const logicalLine of source.split(LINE_BREAK_RE)) {
|
|
111
|
+
wrapLogicalLine(logicalLine, first, cont, lines);
|
|
112
|
+
}
|
|
113
|
+
return lines.length ? lines : [""];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function wrapLogicalLine(line, firstWidth, contWidth, out) {
|
|
117
|
+
const tokens = tokenizeLine(line);
|
|
118
|
+
let available = firstWidth;
|
|
119
|
+
let width = 0;
|
|
120
|
+
let body = "";
|
|
121
|
+
let hasContent = false;
|
|
122
|
+
let activeCodes = [];
|
|
123
|
+
|
|
124
|
+
const finishLine = () => {
|
|
125
|
+
out.push(`${body}${body.includes("\x1b[") ? "\x1b[0m" : ""}`);
|
|
126
|
+
body = activeCodes.join("");
|
|
127
|
+
width = 0;
|
|
128
|
+
hasContent = false;
|
|
129
|
+
available = contWidth;
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
for (let index = 0; index < tokens.length; index += 1) {
|
|
133
|
+
const token = tokens[index];
|
|
134
|
+
if (token.kind === "ansi") {
|
|
135
|
+
applyAnsiToState(token.text, activeCodes);
|
|
136
|
+
body += token.text;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
let text = token.text;
|
|
140
|
+
let tokenWidth = token.width;
|
|
141
|
+
for (;;) {
|
|
142
|
+
if (width + tokenWidth <= available) {
|
|
143
|
+
body += text;
|
|
144
|
+
width += tokenWidth;
|
|
145
|
+
if (token.kind !== "space") {
|
|
146
|
+
hasContent = true;
|
|
147
|
+
}
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
if (token.kind === "space") {
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
if (!hasContent && width === 0 && tokenWidth > available) {
|
|
154
|
+
const take = takeStartCells(text, available);
|
|
155
|
+
if (!take || take === text) {
|
|
156
|
+
body += text;
|
|
157
|
+
width += tokenWidth;
|
|
158
|
+
hasContent = true;
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
body += take;
|
|
162
|
+
width += textWidth(take);
|
|
163
|
+
hasContent = true;
|
|
164
|
+
text = text.slice(take.length);
|
|
165
|
+
tokenWidth -= textWidth(take);
|
|
166
|
+
finishLine();
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
finishLine();
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
out.push(`${body}${body.includes("\x1b[") ? "\x1b[0m" : ""}`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function tokenizeLine(line) {
|
|
176
|
+
const tokens = [];
|
|
177
|
+
let word = "";
|
|
178
|
+
let wordWidth = 0;
|
|
179
|
+
let spaces = "";
|
|
180
|
+
let spaceCount = 0;
|
|
181
|
+
|
|
182
|
+
const flushWord = () => {
|
|
183
|
+
if (word) {
|
|
184
|
+
tokens.push({ kind: "word", text: word, width: wordWidth });
|
|
185
|
+
word = "";
|
|
186
|
+
wordWidth = 0;
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
const flushSpaces = () => {
|
|
190
|
+
if (spaces) {
|
|
191
|
+
tokens.push({ kind: "space", text: spaces, width: spaceCount });
|
|
192
|
+
spaces = "";
|
|
193
|
+
spaceCount = 0;
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
let index = 0;
|
|
198
|
+
while (index < line.length) {
|
|
199
|
+
const char = line[index];
|
|
200
|
+
if (char === "\x1b") {
|
|
201
|
+
const end = findCsiEnd(line, index);
|
|
202
|
+
if (end !== -1) {
|
|
203
|
+
flushWord();
|
|
204
|
+
flushSpaces();
|
|
205
|
+
tokens.push({ kind: "ansi", text: line.slice(index, end), width: 0 });
|
|
206
|
+
index = end;
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
const codePoint = line.codePointAt(index);
|
|
211
|
+
const segment = String.fromCodePoint(codePoint);
|
|
212
|
+
if (segment === " ") {
|
|
213
|
+
flushWord();
|
|
214
|
+
spaces += segment;
|
|
215
|
+
spaceCount += 1;
|
|
216
|
+
} else {
|
|
217
|
+
flushSpaces();
|
|
218
|
+
const width = segmentWidth(segment);
|
|
219
|
+
if (width > 1) {
|
|
220
|
+
flushWord();
|
|
221
|
+
tokens.push({ kind: "word", text: segment, width });
|
|
222
|
+
} else {
|
|
223
|
+
word += segment;
|
|
224
|
+
wordWidth += width;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
index += segment.length;
|
|
228
|
+
}
|
|
229
|
+
flushWord();
|
|
230
|
+
flushSpaces();
|
|
231
|
+
return tokens;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function findCsiEnd(value, start) {
|
|
235
|
+
if (start + 1 >= value.length || value[start + 1] !== "[") {
|
|
236
|
+
return -1;
|
|
237
|
+
}
|
|
238
|
+
for (let index = start + 2; index < value.length; index += 1) {
|
|
239
|
+
const code = value.charCodeAt(index);
|
|
240
|
+
if (code >= 0x40 && code <= 0x7e) {
|
|
241
|
+
return index + 1;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return -1;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function applyAnsiToState(sequence, activeCodes) {
|
|
248
|
+
const params = sequence.slice(2, -1);
|
|
249
|
+
if (params === "" || params === "0") {
|
|
250
|
+
activeCodes.length = 0;
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
activeCodes.push(sequence);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function middleClipCells(value, maxWidth) {
|
|
257
|
+
const text = String(value || "");
|
|
258
|
+
if (textWidth(text) <= maxWidth) {
|
|
259
|
+
return text;
|
|
260
|
+
}
|
|
261
|
+
const suffix = "...";
|
|
262
|
+
const available = Math.max(0, maxWidth - textWidth(suffix));
|
|
263
|
+
const headWidth = Math.ceil(available / 2);
|
|
264
|
+
const tailWidth = Math.floor(available / 2);
|
|
265
|
+
return `${takeStartCells(text, headWidth)}${suffix}${takeEndCells(text, tailWidth)}`;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function takeStartCells(value, maxWidth) {
|
|
269
|
+
let output = "";
|
|
270
|
+
let width = 0;
|
|
271
|
+
for (const segment of graphemes(value)) {
|
|
272
|
+
const nextWidth = segmentWidth(segment);
|
|
273
|
+
if (width + nextWidth > maxWidth) {
|
|
274
|
+
break;
|
|
275
|
+
}
|
|
276
|
+
output += segment;
|
|
277
|
+
width += nextWidth;
|
|
278
|
+
}
|
|
279
|
+
return output;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function takeEndCells(value, maxWidth) {
|
|
283
|
+
let output = "";
|
|
284
|
+
let width = 0;
|
|
285
|
+
for (const segment of graphemes(value).reverse()) {
|
|
286
|
+
const nextWidth = segmentWidth(segment);
|
|
287
|
+
if (width + nextWidth > maxWidth) {
|
|
288
|
+
break;
|
|
289
|
+
}
|
|
290
|
+
output = `${segment}${output}`;
|
|
291
|
+
width += nextWidth;
|
|
292
|
+
}
|
|
293
|
+
return output;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function segmentWidth(segment) {
|
|
297
|
+
const text = stripAnsi(segment);
|
|
298
|
+
if (!text || isZeroWidth(text)) {
|
|
299
|
+
return 0;
|
|
300
|
+
}
|
|
301
|
+
if (isEmoji(text)) {
|
|
302
|
+
return 2;
|
|
303
|
+
}
|
|
304
|
+
return Array.from(text).some((char) => isWideCodePoint(char.codePointAt(0))) ? 2 : 1;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function positiveWidth(value) {
|
|
308
|
+
const width = Number(value);
|
|
309
|
+
return Number.isFinite(width) && width > 0 ? Math.floor(width) : 1;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function isZeroWidth(text) {
|
|
313
|
+
return /^[\u0300-\u036f\u0483-\u0489\u200b-\u200f\u20d0-\u20ff\ufe00-\ufe0f]+$/u.test(text);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function isEmoji(text) {
|
|
317
|
+
return /^[0-9#*]\ufe0f?\u20e3$/u.test(text)
|
|
318
|
+
|| /[\u{1f1e6}-\u{1f1ff}]/u.test(text)
|
|
319
|
+
|| /\p{Extended_Pictographic}/u.test(text)
|
|
320
|
+
|| text.includes("\u200d")
|
|
321
|
+
|| text.includes("\ufe0f");
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function isWideCodePoint(codePoint) {
|
|
325
|
+
return (codePoint >= 0x1100 && codePoint <= 0x115f)
|
|
326
|
+
|| codePoint === 0x2329
|
|
327
|
+
|| codePoint === 0x232a
|
|
328
|
+
|| (codePoint >= 0x2e80 && codePoint <= 0xa4cf && codePoint !== 0x303f)
|
|
329
|
+
|| (codePoint >= 0xac00 && codePoint <= 0xd7a3)
|
|
330
|
+
|| (codePoint >= 0xf900 && codePoint <= 0xfaff)
|
|
331
|
+
|| (codePoint >= 0xfe10 && codePoint <= 0xfe19)
|
|
332
|
+
|| (codePoint >= 0xfe30 && codePoint <= 0xfe6f)
|
|
333
|
+
|| (codePoint >= 0xff00 && codePoint <= 0xff60)
|
|
334
|
+
|| (codePoint >= 0xffe0 && codePoint <= 0xffe6);
|
|
335
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { themeOptions } from "./theme.js";
|
|
2
|
+
|
|
3
|
+
export function createThemeMenuState() {
|
|
4
|
+
const items = themeOptions();
|
|
5
|
+
let selected = Math.max(0, items.findIndex((item) => item.current));
|
|
6
|
+
return {
|
|
7
|
+
items() {
|
|
8
|
+
return items;
|
|
9
|
+
},
|
|
10
|
+
selectedIndex() {
|
|
11
|
+
return selected;
|
|
12
|
+
},
|
|
13
|
+
selectedTheme() {
|
|
14
|
+
return items[selected] || null;
|
|
15
|
+
},
|
|
16
|
+
handleKey(key = {}) {
|
|
17
|
+
if (!items.length) {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
if (key.name === "up") {
|
|
21
|
+
selected = selected <= 0 ? items.length - 1 : selected - 1;
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
if (key.name === "down") {
|
|
25
|
+
selected = selected >= items.length - 1 ? 0 : selected + 1;
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
return false;
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
package/lib/theme.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
// Single source of truth for CLI colors: Catppuccin flavors mapped to a
|
|
2
|
+
// handful of semantic roles. Every painter re-evaluates color support on
|
|
3
|
+
// each call so runtime toggles (isTTY / NO_COLOR) behave like before.
|
|
4
|
+
const FLAVORS = {
|
|
5
|
+
latte: {
|
|
6
|
+
label: "Latte",
|
|
7
|
+
accent: "#1e66f5",
|
|
8
|
+
success: "#40a02b",
|
|
9
|
+
danger: "#d20f39",
|
|
10
|
+
warning: "#df8e1d",
|
|
11
|
+
notice: "#8839ef",
|
|
12
|
+
path: "#209fb5",
|
|
13
|
+
code: "#fe640b",
|
|
14
|
+
fence: "#04a5e5",
|
|
15
|
+
dim: "#6c6f85",
|
|
16
|
+
},
|
|
17
|
+
frappe: {
|
|
18
|
+
label: "Frappé",
|
|
19
|
+
accent: "#8caaee",
|
|
20
|
+
success: "#a6d189",
|
|
21
|
+
danger: "#e78284",
|
|
22
|
+
warning: "#e5c890",
|
|
23
|
+
notice: "#ca9ee6",
|
|
24
|
+
path: "#85c1dc",
|
|
25
|
+
code: "#ef9f76",
|
|
26
|
+
fence: "#99d1db",
|
|
27
|
+
dim: "#a5adce",
|
|
28
|
+
},
|
|
29
|
+
macchiato: {
|
|
30
|
+
label: "Macchiato",
|
|
31
|
+
accent: "#8aadf4",
|
|
32
|
+
success: "#a6da95",
|
|
33
|
+
danger: "#ed8796",
|
|
34
|
+
warning: "#eed49f",
|
|
35
|
+
notice: "#c6a0f6",
|
|
36
|
+
path: "#7dc4e4",
|
|
37
|
+
code: "#f5a97f",
|
|
38
|
+
fence: "#91d7e3",
|
|
39
|
+
dim: "#a5adcb",
|
|
40
|
+
},
|
|
41
|
+
mocha: {
|
|
42
|
+
label: "Mocha",
|
|
43
|
+
accent: "#89b4fa",
|
|
44
|
+
success: "#a6e3a1",
|
|
45
|
+
danger: "#f38ba8",
|
|
46
|
+
warning: "#f9e2af",
|
|
47
|
+
notice: "#cba6f7",
|
|
48
|
+
path: "#74c7ec",
|
|
49
|
+
code: "#fab387",
|
|
50
|
+
fence: "#89dceb",
|
|
51
|
+
dim: "#a6adc8",
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export const DEFAULT_THEME = "mocha";
|
|
56
|
+
|
|
57
|
+
let activeName = DEFAULT_THEME;
|
|
58
|
+
|
|
59
|
+
function enabled() {
|
|
60
|
+
return Boolean(process.stdout.isTTY) && !process.env.NO_COLOR;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function truecolor(hex) {
|
|
64
|
+
const value = Number.parseInt(hex.slice(1), 16);
|
|
65
|
+
return `38;2;${(value >> 16) & 255};${(value >> 8) & 255};${value & 255}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function wrap(code, text, force) {
|
|
69
|
+
const body = String(text || "");
|
|
70
|
+
if (!body || (!force && !enabled())) {
|
|
71
|
+
return body;
|
|
72
|
+
}
|
|
73
|
+
return `\x1b[${code}m${body}\x1b[0m`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const ROLE_KEYS = ["accent", "success", "danger", "warning", "notice", "path", "code", "fence"];
|
|
77
|
+
|
|
78
|
+
// paint: honors isTTY / NO_COLOR. paintRaw: always emits — for callers that
|
|
79
|
+
// gate colors themselves (AssistantRenderer's color option, TTY-only blocks).
|
|
80
|
+
function buildPainters(force) {
|
|
81
|
+
const painters = {
|
|
82
|
+
dim: (text) => wrap("2", text, force),
|
|
83
|
+
bold: (text) => wrap("1", text, force),
|
|
84
|
+
};
|
|
85
|
+
for (const key of ROLE_KEYS) {
|
|
86
|
+
painters[key] = (text) => wrap(truecolor(FLAVORS[activeName][key]), text, force);
|
|
87
|
+
}
|
|
88
|
+
return painters;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export const paint = buildPainters(false);
|
|
92
|
+
export const paintRaw = buildPainters(true);
|
|
93
|
+
|
|
94
|
+
export function themeNames() {
|
|
95
|
+
return Object.keys(FLAVORS);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function currentTheme() {
|
|
99
|
+
const flavor = FLAVORS[activeName];
|
|
100
|
+
return { name: activeName, label: flavor.label };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function setTheme(name) {
|
|
104
|
+
const key = String(name || "").trim().toLowerCase();
|
|
105
|
+
if (!FLAVORS[key]) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
activeName = key;
|
|
109
|
+
return currentTheme();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function resetTheme() {
|
|
113
|
+
activeName = DEFAULT_THEME;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function themeOptions() {
|
|
117
|
+
return Object.entries(FLAVORS).map(([name, flavor]) => ({
|
|
118
|
+
name,
|
|
119
|
+
label: flavor.label,
|
|
120
|
+
current: name === activeName,
|
|
121
|
+
}));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Eight-color preview rendered in the target flavor regardless of active
|
|
125
|
+
// theme: one narrow cell per Catppuccin hue family.
|
|
126
|
+
const SWATCH_ORDER = ["danger", "code", "warning", "success", "fence", "path", "accent", "notice"];
|
|
127
|
+
|
|
128
|
+
export function flavorSwatch(name) {
|
|
129
|
+
const flavor = FLAVORS[String(name || "").trim().toLowerCase()];
|
|
130
|
+
if (!flavor) {
|
|
131
|
+
return "";
|
|
132
|
+
}
|
|
133
|
+
return SWATCH_ORDER.map((role) => wrap(truecolor(flavor[role]), "█", true)).join("");
|
|
134
|
+
}
|