brito-cli 0.0.0-stage → 0.3.2
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/.brito/agents/reviewer.md +7 -0
- package/LICENSE +75 -0
- package/README.md +166 -2
- package/README.pt-BR.md +167 -0
- package/dist/agent/agents.js +65 -0
- package/dist/agent/claude-cli.js +216 -0
- package/dist/agent/context.js +40 -0
- package/dist/agent/loop.js +125 -0
- package/dist/agent/prompt.js +54 -0
- package/dist/cli.js +568 -0
- package/dist/graph/aliases.js +196 -0
- package/dist/graph/indexer.js +438 -0
- package/dist/graph/langs.js +526 -0
- package/dist/graph/sfc.js +52 -0
- package/dist/graph/store.js +96 -0
- package/dist/mcp/server.js +98 -0
- package/dist/model.js +136 -0
- package/dist/permissions.js +39 -0
- package/dist/repl.js +297 -0
- package/dist/skills/consolidate.js +76 -0
- package/dist/skills/describe.js +14 -0
- package/dist/skills/journal.js +72 -0
- package/dist/skills/learn-cli.js +55 -0
- package/dist/skills/learn.js +217 -0
- package/dist/skills/store.js +234 -0
- package/dist/tools/bash.js +17 -0
- package/dist/tools/delegate.js +61 -0
- package/dist/tools/fs.js +91 -0
- package/dist/tools/graph.js +117 -0
- package/dist/tools/plan.js +17 -0
- package/dist/tools/registry.js +56 -0
- package/dist/tools/skills.js +58 -0
- package/dist/ui/banner.js +138 -0
- package/dist/ui/dock.js +616 -0
- package/dist/ui/theme.js +63 -0
- package/dist/ui/ui.js +426 -0
- package/dist/ui/web-art.js +131 -0
- package/dist/util/diff.js +36 -0
- package/dist/util/frontmatter.js +16 -0
- package/package.json +66 -4
package/dist/ui/dock.js
ADDED
|
@@ -0,0 +1,616 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import stringWidth from "string-width";
|
|
4
|
+
import { c, stripAnsi, truncAnsi } from "./theme.js";
|
|
5
|
+
const seg = new Intl.Segmenter(undefined, { granularity: "grapheme" });
|
|
6
|
+
const graphemes = (s) => Array.from(seg.segment(s), (x) => x.segment);
|
|
7
|
+
const ANSI = /\x1b\[[0-9;]*[A-Za-z]/y;
|
|
8
|
+
const MAX_INPUT_ROWS = 8;
|
|
9
|
+
const MAX_MENU = 7;
|
|
10
|
+
const PASTE_RE = /\[colado #(\d+): [^\]]*\]/g;
|
|
11
|
+
export class Dock {
|
|
12
|
+
opts;
|
|
13
|
+
onSubmit = () => { };
|
|
14
|
+
onInterrupt = () => { };
|
|
15
|
+
onEOF = () => { };
|
|
16
|
+
onCycleMode = () => { };
|
|
17
|
+
rows = process.stdout.rows || 24;
|
|
18
|
+
cols = process.stdout.columns || 80;
|
|
19
|
+
// cursor virtual da saída (1-based) e área ocupada pelo dock
|
|
20
|
+
outRow = 1;
|
|
21
|
+
outCol = 1;
|
|
22
|
+
skipNL = false;
|
|
23
|
+
top = 1;
|
|
24
|
+
height = 0;
|
|
25
|
+
spinner = null;
|
|
26
|
+
busy = false;
|
|
27
|
+
promptOverride = null;
|
|
28
|
+
buf = [];
|
|
29
|
+
cur = 0;
|
|
30
|
+
history = [];
|
|
31
|
+
hIdx = -1;
|
|
32
|
+
draft = "";
|
|
33
|
+
pastes = new Map();
|
|
34
|
+
pasteN = 0;
|
|
35
|
+
pasting = false;
|
|
36
|
+
pasteBuf = "";
|
|
37
|
+
menuSel = 0;
|
|
38
|
+
picker = null;
|
|
39
|
+
dsr;
|
|
40
|
+
started = false;
|
|
41
|
+
onData = (d) => this.feed(d);
|
|
42
|
+
onResize = () => {
|
|
43
|
+
this.rows = process.stdout.rows || 24;
|
|
44
|
+
this.cols = process.stdout.columns || 80;
|
|
45
|
+
this.outRow = Math.min(this.outRow, this.rows);
|
|
46
|
+
process.stdout.write(this.paintSeq(false));
|
|
47
|
+
};
|
|
48
|
+
constructor(opts) {
|
|
49
|
+
this.opts = opts;
|
|
50
|
+
if (opts.historyFile) {
|
|
51
|
+
try {
|
|
52
|
+
this.history = fs.readFileSync(opts.historyFile, "utf8").split("\n").filter(Boolean).map((l) => JSON.parse(l)).slice(-500);
|
|
53
|
+
}
|
|
54
|
+
catch { /* sem histórico */ }
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
// ---------------------------------------------------------------- ciclo de vida
|
|
58
|
+
async start() {
|
|
59
|
+
process.stdin.setEncoding("utf8");
|
|
60
|
+
process.stdin.setRawMode(true);
|
|
61
|
+
process.stdin.on("data", this.onData);
|
|
62
|
+
process.stdin.resume();
|
|
63
|
+
process.stdout.on("resize", this.onResize);
|
|
64
|
+
process.on("exit", () => this.restoreTerminal());
|
|
65
|
+
process.stdout.write("\x1b[?2004h"); // bracketed paste
|
|
66
|
+
const [r, col] = await this.getCursor();
|
|
67
|
+
this.outRow = Math.min(r, this.rows);
|
|
68
|
+
this.outCol = Math.min(col, this.cols);
|
|
69
|
+
this.started = true;
|
|
70
|
+
process.stdout.write(this.paintSeq(false));
|
|
71
|
+
}
|
|
72
|
+
/** Remove o dock e deixa o cursor na linha seguinte à última saída. */
|
|
73
|
+
stop() {
|
|
74
|
+
if (!this.started)
|
|
75
|
+
return;
|
|
76
|
+
this.started = false;
|
|
77
|
+
let out = this.clearSeq();
|
|
78
|
+
out += `\x1b[${this.outRow};${this.outCol}H`;
|
|
79
|
+
if (this.outCol > 1)
|
|
80
|
+
out += "\r\n";
|
|
81
|
+
process.stdout.write(out);
|
|
82
|
+
this.restoreTerminal();
|
|
83
|
+
process.stdin.off("data", this.onData);
|
|
84
|
+
process.stdout.off("resize", this.onResize);
|
|
85
|
+
process.stdin.pause();
|
|
86
|
+
}
|
|
87
|
+
restoreTerminal() {
|
|
88
|
+
try {
|
|
89
|
+
process.stdout.write("\x1b[?2004l\x1b[?25h");
|
|
90
|
+
process.stdin.setRawMode?.(false);
|
|
91
|
+
}
|
|
92
|
+
catch { /* já fechado */ }
|
|
93
|
+
}
|
|
94
|
+
getCursor() {
|
|
95
|
+
return new Promise((resolve) => {
|
|
96
|
+
const t = setTimeout(() => { this.dsr = undefined; resolve([this.rows, 1]); }, 400);
|
|
97
|
+
this.dsr = (r, col) => { clearTimeout(t); this.dsr = undefined; resolve([r, col]); };
|
|
98
|
+
process.stdout.write("\x1b[6n");
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
// ---------------------------------------------------------------- API pública
|
|
102
|
+
/** Escreve texto acima do dock (aceita ANSI e quebras de linha). */
|
|
103
|
+
write(text) {
|
|
104
|
+
if (!this.started) {
|
|
105
|
+
process.stdout.write(text);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
let out = this.clearSeq();
|
|
109
|
+
if (this.skipNL && text.startsWith("\n"))
|
|
110
|
+
text = text.slice(1);
|
|
111
|
+
this.skipNL = false;
|
|
112
|
+
out += `\x1b[${this.outRow};${this.outCol}H`;
|
|
113
|
+
let row = this.outRow, col = this.outCol;
|
|
114
|
+
const down = () => { row = Math.min(row + 1, this.rows); col = 1; };
|
|
115
|
+
text = text.replace(/\r/g, "").replace(/\t/g, " ");
|
|
116
|
+
let i = 0;
|
|
117
|
+
while (i < text.length) {
|
|
118
|
+
ANSI.lastIndex = i;
|
|
119
|
+
const m = ANSI.exec(text);
|
|
120
|
+
if (m) {
|
|
121
|
+
out += m[0];
|
|
122
|
+
i += m[0].length;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (text[i] === "\n") {
|
|
126
|
+
out += "\r\n";
|
|
127
|
+
down();
|
|
128
|
+
i++;
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
// próximo grafema (até o próximo ESC ou \n)
|
|
132
|
+
let j = i;
|
|
133
|
+
while (j < text.length && text[j] !== "\n" && text[j] !== "\x1b")
|
|
134
|
+
j++;
|
|
135
|
+
if (j === i) {
|
|
136
|
+
out += text[i];
|
|
137
|
+
i++;
|
|
138
|
+
continue;
|
|
139
|
+
} // ESC solto
|
|
140
|
+
for (const g of graphemes(text.slice(i, j))) {
|
|
141
|
+
const w = stringWidth(g);
|
|
142
|
+
if (w === 0) {
|
|
143
|
+
out += g;
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (col + w - 1 > this.cols) {
|
|
147
|
+
out += "\r\n";
|
|
148
|
+
down();
|
|
149
|
+
}
|
|
150
|
+
out += g;
|
|
151
|
+
col += w;
|
|
152
|
+
}
|
|
153
|
+
i = j;
|
|
154
|
+
}
|
|
155
|
+
if (col > this.cols) {
|
|
156
|
+
out += "\r\n";
|
|
157
|
+
down();
|
|
158
|
+
this.skipNL = true;
|
|
159
|
+
}
|
|
160
|
+
this.outRow = row;
|
|
161
|
+
this.outCol = col;
|
|
162
|
+
process.stdout.write(out + this.paintSeq(true));
|
|
163
|
+
}
|
|
164
|
+
setSpinner(text) {
|
|
165
|
+
if (text === this.spinner)
|
|
166
|
+
return;
|
|
167
|
+
this.spinner = text;
|
|
168
|
+
if (this.started)
|
|
169
|
+
process.stdout.write(this.paintSeq(false));
|
|
170
|
+
}
|
|
171
|
+
setBusy(b) { this.busy = b; if (this.started)
|
|
172
|
+
process.stdout.write(this.paintSeq(false)); }
|
|
173
|
+
/** Troca o prompt (ex.: pergunta de permissão). null restaura o padrão. */
|
|
174
|
+
setPrompt(p) { this.promptOverride = p; if (this.started)
|
|
175
|
+
process.stdout.write(this.paintSeq(false)); }
|
|
176
|
+
/** Menu de escolha no lugar da caixa de entrada: ↑↓ navegam, Enter confirma, Esc/Ctrl+C cancelam. */
|
|
177
|
+
select(title, options, current = 0) {
|
|
178
|
+
return new Promise((resolve) => {
|
|
179
|
+
this.picker = { title, options, sel: Math.max(0, Math.min(current, options.length - 1)), done: resolve };
|
|
180
|
+
this.repaint();
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
closePicker(i) {
|
|
184
|
+
const p = this.picker;
|
|
185
|
+
if (!p)
|
|
186
|
+
return;
|
|
187
|
+
this.picker = null;
|
|
188
|
+
this.repaint();
|
|
189
|
+
p.done(i);
|
|
190
|
+
}
|
|
191
|
+
hasText() { return this.buf.length > 0; }
|
|
192
|
+
clearInput() { this.buf = []; this.cur = 0; this.hIdx = -1; this.menuSel = 0; this.repaint(); }
|
|
193
|
+
repaint() { if (this.started)
|
|
194
|
+
process.stdout.write(this.paintSeq(false)); }
|
|
195
|
+
// ---------------------------------------------------------------- layout e pintura
|
|
196
|
+
clearSeq() {
|
|
197
|
+
let s = "";
|
|
198
|
+
for (let r = 0; r < this.height; r++)
|
|
199
|
+
s += `\x1b[${this.top + r};1H\x1b[2K`;
|
|
200
|
+
return s;
|
|
201
|
+
}
|
|
202
|
+
promptStr() {
|
|
203
|
+
return this.promptOverride ?? (this.busy ? c.light("◉ ") : c.bold(c.light("❯ ")));
|
|
204
|
+
}
|
|
205
|
+
menuItems() {
|
|
206
|
+
const t = this.buf.join("");
|
|
207
|
+
if (!t.startsWith("/") || /\s/.test(t))
|
|
208
|
+
return [];
|
|
209
|
+
const m = this.opts.commands.filter((x) => x.name.startsWith(t.toLowerCase()));
|
|
210
|
+
return m.length === 1 && m[0].name === t ? [] : m.slice(0, MAX_MENU);
|
|
211
|
+
}
|
|
212
|
+
/** Quebra o buffer em linhas visuais e acha a posição do cursor. */
|
|
213
|
+
layoutInput(width) {
|
|
214
|
+
const lines = [""];
|
|
215
|
+
let r = 0, col = 0, curRow = 0, curCol = 0;
|
|
216
|
+
for (let i = 0; i <= this.buf.length; i++) {
|
|
217
|
+
if (i === this.cur) {
|
|
218
|
+
curRow = r;
|
|
219
|
+
curCol = col;
|
|
220
|
+
}
|
|
221
|
+
if (i === this.buf.length)
|
|
222
|
+
break;
|
|
223
|
+
const g = this.buf[i];
|
|
224
|
+
if (g === "\n") {
|
|
225
|
+
r++;
|
|
226
|
+
col = 0;
|
|
227
|
+
lines.push("");
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
const w = stringWidth(g);
|
|
231
|
+
if (col + w > width) {
|
|
232
|
+
r++;
|
|
233
|
+
col = 0;
|
|
234
|
+
lines.push("");
|
|
235
|
+
if (i === this.cur) {
|
|
236
|
+
curRow = r;
|
|
237
|
+
curCol = 0;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
lines[r] += g;
|
|
241
|
+
col += w;
|
|
242
|
+
}
|
|
243
|
+
return { lines, curRow, curCol };
|
|
244
|
+
}
|
|
245
|
+
compose() {
|
|
246
|
+
const pStr = this.promptStr();
|
|
247
|
+
const pw = stringWidth(stripAnsi(pStr));
|
|
248
|
+
const width = Math.max(10, this.cols - pw);
|
|
249
|
+
const { lines, curRow, curCol } = this.layoutInput(width);
|
|
250
|
+
const visible = Math.min(lines.length, MAX_INPUT_ROWS);
|
|
251
|
+
let start = Math.max(0, Math.min(lines.length - visible, curRow - Math.floor(visible / 2)));
|
|
252
|
+
if (curRow < start)
|
|
253
|
+
start = curRow;
|
|
254
|
+
if (curRow >= start + visible)
|
|
255
|
+
start = curRow - visible + 1;
|
|
256
|
+
const out = [];
|
|
257
|
+
if (this.spinner)
|
|
258
|
+
out.push(this.spinner);
|
|
259
|
+
out.push(this.opts.status());
|
|
260
|
+
const rule = c.accent("─".repeat(this.cols));
|
|
261
|
+
if (this.picker) {
|
|
262
|
+
const p = this.picker;
|
|
263
|
+
out.push(rule, c.bold(c.light(" " + p.title)));
|
|
264
|
+
const w = Math.max(...p.options.map((o) => o.label.length));
|
|
265
|
+
p.options.forEach((o, k) => {
|
|
266
|
+
const row = ` ${o.label.padEnd(w)}${o.hint ? " " + o.hint : ""}`;
|
|
267
|
+
out.push(k === p.sel ? c.bold(c.light("▸" + row)) : c.muted(" " + row));
|
|
268
|
+
});
|
|
269
|
+
out.push(c.dim(" ↑↓ escolhe · Enter confirma · Esc cancela"));
|
|
270
|
+
return { out, cursor: { row: out.length, col: 1 } };
|
|
271
|
+
}
|
|
272
|
+
out.push(rule);
|
|
273
|
+
const inputStart = out.length;
|
|
274
|
+
for (let k = 0; k < visible; k++) {
|
|
275
|
+
const idx = start + k;
|
|
276
|
+
const lead = idx === 0 ? pStr : " ".repeat(pw);
|
|
277
|
+
let text = lines[idx];
|
|
278
|
+
if (idx === 0 && this.buf.length === 0) {
|
|
279
|
+
text = c.dim(this.promptOverride ? "" : this.busy ? "digite para enfileirar · Ctrl+C interrompe" : "mensagem · / comandos · !cmd shell · Alt+Enter nova linha · ⇧Tab modo");
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
// escrita do usuário: negrito vermelho (mesma cor de "tudo liberado"); marcadores de colagem em âmbar
|
|
283
|
+
text = text.split(/(\[colado #\d+: [^\]]*\])/).map((p, i) => (i % 2 ? c.rgb([255, 200, 120])(p) : p ? c.bold(c.err(p)) : p)).join("");
|
|
284
|
+
}
|
|
285
|
+
out.push(lead + text);
|
|
286
|
+
}
|
|
287
|
+
out.push(rule);
|
|
288
|
+
const items = this.menuItems();
|
|
289
|
+
if (items.length) {
|
|
290
|
+
this.menuSel = Math.min(this.menuSel, items.length - 1);
|
|
291
|
+
const nameW = Math.max(...items.map((x) => x.name.length));
|
|
292
|
+
items.forEach((it, k) => {
|
|
293
|
+
const row = ` ${it.name.padEnd(nameW)} ${it.desc}`;
|
|
294
|
+
out.push(k === this.menuSel ? c.bold(c.light("▸" + row)) : c.muted(" " + row));
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
return { out, cursor: { row: inputStart + (curRow - start), col: pw + Math.min(curCol, width) + 1 } };
|
|
298
|
+
}
|
|
299
|
+
/** Sequência que (re)pinta o dock logo abaixo do cursor virtual de saída. */
|
|
300
|
+
paintSeq(alreadyCleared) {
|
|
301
|
+
let s = alreadyCleared ? "" : this.clearSeq();
|
|
302
|
+
const { out, cursor } = this.compose();
|
|
303
|
+
const h = Math.min(out.length, this.rows);
|
|
304
|
+
const visibleLines = out.slice(out.length - h);
|
|
305
|
+
let base = this.outCol > 1 ? this.outRow + 1 : this.outRow;
|
|
306
|
+
const overflow = base + h - 1 - this.rows;
|
|
307
|
+
if (overflow > 0) {
|
|
308
|
+
s += `\x1b[${this.rows};1H${"\n".repeat(overflow)}`; // rola a tela inteira
|
|
309
|
+
base -= overflow;
|
|
310
|
+
this.outRow = Math.max(1, this.outRow - overflow);
|
|
311
|
+
}
|
|
312
|
+
for (let r = 0; r < h; r++) {
|
|
313
|
+
const line = visibleLines[r];
|
|
314
|
+
s += `\x1b[${base + r};1H\x1b[2K${stripAnsi(line).length > this.cols ? truncAnsi(line, this.cols) : line}`;
|
|
315
|
+
}
|
|
316
|
+
this.top = base;
|
|
317
|
+
this.height = h;
|
|
318
|
+
const row = base + cursor.row - (out.length - h);
|
|
319
|
+
s += `\x1b[${Math.min(row, this.rows)};${Math.min(cursor.col, this.cols)}H`;
|
|
320
|
+
return s;
|
|
321
|
+
}
|
|
322
|
+
// ---------------------------------------------------------------- entrada
|
|
323
|
+
feedPicker(data) {
|
|
324
|
+
const p = this.picker;
|
|
325
|
+
const n = p.options.length;
|
|
326
|
+
if (/^\x1b\[[AB]|^\x1bO[AB]/.test(data) || data === "k" || data === "j") {
|
|
327
|
+
p.sel = (p.sel + (data.endsWith("A") || data === "k" ? -1 : 1) + n) % n;
|
|
328
|
+
this.repaint();
|
|
329
|
+
}
|
|
330
|
+
else if (data === "\r" || data === "\n")
|
|
331
|
+
this.closePicker(p.sel);
|
|
332
|
+
else if (data === "\x1b" || data === "\x03")
|
|
333
|
+
this.closePicker(null);
|
|
334
|
+
}
|
|
335
|
+
feed(data) {
|
|
336
|
+
if (this.picker) {
|
|
337
|
+
this.feedPicker(data);
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
let i = 0;
|
|
341
|
+
while (i < data.length) {
|
|
342
|
+
if (this.pasting) {
|
|
343
|
+
const end = data.indexOf("\x1b[201~", i);
|
|
344
|
+
if (end === -1) {
|
|
345
|
+
this.pasteBuf += data.slice(i);
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
this.pasteBuf += data.slice(i, end);
|
|
349
|
+
this.finishPaste();
|
|
350
|
+
this.pasting = false;
|
|
351
|
+
i = end + 6;
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
const rest = data.slice(i);
|
|
355
|
+
if (rest.startsWith("\x1b[200~")) {
|
|
356
|
+
this.pasting = true;
|
|
357
|
+
this.pasteBuf = "";
|
|
358
|
+
i += 6;
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
let m = rest.match(/^\x1b\[(\d+);(\d+)R/);
|
|
362
|
+
if (m) {
|
|
363
|
+
this.dsr?.(+m[1], +m[2]);
|
|
364
|
+
i += m[0].length;
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
m = rest.match(/^\x1b\[([\d;]*)([A-Za-z~])/);
|
|
368
|
+
if (m) {
|
|
369
|
+
this.csi(m[1], m[2]);
|
|
370
|
+
i += m[0].length;
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
m = rest.match(/^\x1bO([A-Za-z])/);
|
|
374
|
+
if (m) {
|
|
375
|
+
this.csi("", m[1]);
|
|
376
|
+
i += m[0].length;
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
if (rest[0] === "\x1b") {
|
|
380
|
+
if (rest.length > 1) {
|
|
381
|
+
this.alt(rest[1]);
|
|
382
|
+
i += 2;
|
|
383
|
+
}
|
|
384
|
+
else {
|
|
385
|
+
this.menuSel = 0;
|
|
386
|
+
i++;
|
|
387
|
+
}
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
const code = rest.charCodeAt(0);
|
|
391
|
+
if (code < 0x20 || code === 0x7f) {
|
|
392
|
+
this.ctrl(code);
|
|
393
|
+
i++;
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
const run = rest.match(/^[^\x00-\x1f\x7f\x1b]+/)[0];
|
|
397
|
+
this.insert(run);
|
|
398
|
+
i += run.length;
|
|
399
|
+
}
|
|
400
|
+
this.repaint();
|
|
401
|
+
}
|
|
402
|
+
insert(text) {
|
|
403
|
+
const gs = graphemes(text);
|
|
404
|
+
this.buf.splice(this.cur, 0, ...gs);
|
|
405
|
+
this.cur += gs.length;
|
|
406
|
+
this.menuSel = 0;
|
|
407
|
+
this.hIdx = -1;
|
|
408
|
+
}
|
|
409
|
+
finishPaste() {
|
|
410
|
+
const text = this.pasteBuf.replace(/\r\n?/g, "\n");
|
|
411
|
+
const lines = text.split("\n").length;
|
|
412
|
+
if (lines >= 2 || text.length > 300) {
|
|
413
|
+
const n = ++this.pasteN;
|
|
414
|
+
this.pastes.set(n, text);
|
|
415
|
+
const size = text.length >= 1000 ? `${(text.length / 1000).toFixed(1)}K` : String(text.length);
|
|
416
|
+
this.insert(`[colado #${n}: ${lines} linhas, ${size} chars]`);
|
|
417
|
+
}
|
|
418
|
+
else
|
|
419
|
+
this.insert(text);
|
|
420
|
+
this.repaint();
|
|
421
|
+
}
|
|
422
|
+
ctrl(code) {
|
|
423
|
+
switch (code) {
|
|
424
|
+
case 0x03:
|
|
425
|
+
this.onInterrupt();
|
|
426
|
+
break; // Ctrl+C
|
|
427
|
+
case 0x04:
|
|
428
|
+
if (this.buf.length === 0)
|
|
429
|
+
this.onEOF();
|
|
430
|
+
else
|
|
431
|
+
this.del();
|
|
432
|
+
break; // Ctrl+D
|
|
433
|
+
case 0x0d:
|
|
434
|
+
this.submit();
|
|
435
|
+
break; // Enter
|
|
436
|
+
case 0x0a:
|
|
437
|
+
this.insert("\n");
|
|
438
|
+
break; // Ctrl+J
|
|
439
|
+
case 0x7f:
|
|
440
|
+
case 0x08:
|
|
441
|
+
if (this.cur > 0) {
|
|
442
|
+
this.buf.splice(--this.cur, 1);
|
|
443
|
+
this.menuSel = 0;
|
|
444
|
+
}
|
|
445
|
+
break;
|
|
446
|
+
case 0x01:
|
|
447
|
+
this.cur = this.lineStart();
|
|
448
|
+
break; // Ctrl+A
|
|
449
|
+
case 0x05:
|
|
450
|
+
this.cur = this.lineEnd();
|
|
451
|
+
break; // Ctrl+E
|
|
452
|
+
case 0x0b:
|
|
453
|
+
this.buf.splice(this.cur, this.lineEnd() - this.cur);
|
|
454
|
+
break; // Ctrl+K
|
|
455
|
+
case 0x15: {
|
|
456
|
+
const s = this.lineStart();
|
|
457
|
+
this.buf.splice(s, this.cur - s);
|
|
458
|
+
this.cur = s;
|
|
459
|
+
break;
|
|
460
|
+
} // Ctrl+U
|
|
461
|
+
case 0x17:
|
|
462
|
+
this.deleteWord();
|
|
463
|
+
break; // Ctrl+W
|
|
464
|
+
case 0x09:
|
|
465
|
+
this.complete();
|
|
466
|
+
break; // Tab
|
|
467
|
+
case 0x0c:
|
|
468
|
+
this.write("");
|
|
469
|
+
break; // Ctrl+L: repinta
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
alt(ch) {
|
|
473
|
+
if (ch === "\r" || ch === "\n")
|
|
474
|
+
this.insert("\n");
|
|
475
|
+
else if (ch === "b")
|
|
476
|
+
this.cur = this.wordLeft();
|
|
477
|
+
else if (ch === "f")
|
|
478
|
+
this.cur = this.wordRight();
|
|
479
|
+
else if (ch === "\x7f")
|
|
480
|
+
this.deleteWord();
|
|
481
|
+
}
|
|
482
|
+
csi(params, final) {
|
|
483
|
+
const mod = Number(params.split(";")[1] ?? 1);
|
|
484
|
+
const ctrlOrAlt = mod === 5 || mod === 3;
|
|
485
|
+
switch (final) {
|
|
486
|
+
case "A":
|
|
487
|
+
if (this.menuItems().length)
|
|
488
|
+
this.menuSel = Math.max(0, this.menuSel - 1);
|
|
489
|
+
else
|
|
490
|
+
this.historyMove(-1);
|
|
491
|
+
break;
|
|
492
|
+
case "B":
|
|
493
|
+
if (this.menuItems().length)
|
|
494
|
+
this.menuSel = Math.min(this.menuItems().length - 1, this.menuSel + 1);
|
|
495
|
+
else
|
|
496
|
+
this.historyMove(1);
|
|
497
|
+
break;
|
|
498
|
+
case "C":
|
|
499
|
+
this.cur = ctrlOrAlt ? this.wordRight() : Math.min(this.buf.length, this.cur + 1);
|
|
500
|
+
break;
|
|
501
|
+
case "D":
|
|
502
|
+
this.cur = ctrlOrAlt ? this.wordLeft() : Math.max(0, this.cur - 1);
|
|
503
|
+
break;
|
|
504
|
+
case "Z":
|
|
505
|
+
this.onCycleMode();
|
|
506
|
+
break; // Shift+Tab
|
|
507
|
+
case "H":
|
|
508
|
+
this.cur = this.lineStart();
|
|
509
|
+
break;
|
|
510
|
+
case "F":
|
|
511
|
+
this.cur = this.lineEnd();
|
|
512
|
+
break;
|
|
513
|
+
case "~":
|
|
514
|
+
if (params.startsWith("3"))
|
|
515
|
+
this.del();
|
|
516
|
+
else if (params.startsWith("1") || params.startsWith("7"))
|
|
517
|
+
this.cur = this.lineStart();
|
|
518
|
+
else if (params.startsWith("4") || params.startsWith("8"))
|
|
519
|
+
this.cur = this.lineEnd();
|
|
520
|
+
break;
|
|
521
|
+
case "u":
|
|
522
|
+
if (params.startsWith("13")) {
|
|
523
|
+
if (mod === 1)
|
|
524
|
+
this.submit();
|
|
525
|
+
else
|
|
526
|
+
this.insert("\n");
|
|
527
|
+
}
|
|
528
|
+
break; // CSI-u: Shift+Enter
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
del() { if (this.cur < this.buf.length)
|
|
532
|
+
this.buf.splice(this.cur, 1); }
|
|
533
|
+
lineStart() { let i = this.cur; while (i > 0 && this.buf[i - 1] !== "\n")
|
|
534
|
+
i--; return i; }
|
|
535
|
+
lineEnd() { let i = this.cur; while (i < this.buf.length && this.buf[i] !== "\n")
|
|
536
|
+
i++; return i; }
|
|
537
|
+
wordLeft() { let i = this.cur; while (i > 0 && /\s/.test(this.buf[i - 1]))
|
|
538
|
+
i--; while (i > 0 && !/\s/.test(this.buf[i - 1]))
|
|
539
|
+
i--; return i; }
|
|
540
|
+
wordRight() { let i = this.cur; while (i < this.buf.length && /\s/.test(this.buf[i]))
|
|
541
|
+
i++; while (i < this.buf.length && !/\s/.test(this.buf[i]))
|
|
542
|
+
i++; return i; }
|
|
543
|
+
deleteWord() { const s = this.wordLeft(); this.buf.splice(s, this.cur - s); this.cur = s; }
|
|
544
|
+
complete() {
|
|
545
|
+
const items = this.menuItems();
|
|
546
|
+
if (!items.length)
|
|
547
|
+
return;
|
|
548
|
+
const pick = items[Math.min(this.menuSel, items.length - 1)];
|
|
549
|
+
this.buf = graphemes(pick.name + " ");
|
|
550
|
+
this.cur = this.buf.length;
|
|
551
|
+
this.menuSel = 0;
|
|
552
|
+
}
|
|
553
|
+
historyMove(dir) {
|
|
554
|
+
// Em entrada multilinha, as setas movem o cursor entre linhas antes de navegar no histórico.
|
|
555
|
+
const text = this.buf.join("");
|
|
556
|
+
if (text.includes("\n")) {
|
|
557
|
+
const s = this.lineStart();
|
|
558
|
+
if (dir === -1 && s > 0) {
|
|
559
|
+
const col = this.cur - s;
|
|
560
|
+
let ps = s - 1;
|
|
561
|
+
while (ps > 0 && this.buf[ps - 1] !== "\n")
|
|
562
|
+
ps--;
|
|
563
|
+
this.cur = Math.min(ps + col, s - 1);
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
const e = this.lineEnd();
|
|
567
|
+
if (dir === 1 && e < this.buf.length) {
|
|
568
|
+
const col = this.cur - s;
|
|
569
|
+
let ne = e + 1;
|
|
570
|
+
while (ne < this.buf.length && this.buf[ne] !== "\n")
|
|
571
|
+
ne++;
|
|
572
|
+
this.cur = Math.min(e + 1 + col, ne);
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
if (!this.history.length)
|
|
577
|
+
return;
|
|
578
|
+
if (this.hIdx === -1) {
|
|
579
|
+
if (dir === 1)
|
|
580
|
+
return;
|
|
581
|
+
this.draft = text;
|
|
582
|
+
this.hIdx = this.history.length;
|
|
583
|
+
}
|
|
584
|
+
this.hIdx = Math.max(0, Math.min(this.history.length, this.hIdx + dir));
|
|
585
|
+
this.buf = graphemes(this.hIdx === this.history.length ? this.draft : this.history[this.hIdx]);
|
|
586
|
+
this.cur = this.buf.length;
|
|
587
|
+
if (this.hIdx === this.history.length)
|
|
588
|
+
this.hIdx = -1;
|
|
589
|
+
}
|
|
590
|
+
submit() {
|
|
591
|
+
// Comando parcial com o menu aberto (ex.: "/ag"): Enter completa o item selecionado antes de enviar.
|
|
592
|
+
if (this.menuItems().length)
|
|
593
|
+
this.complete();
|
|
594
|
+
const raw = this.buf.join("").trimEnd();
|
|
595
|
+
if (!raw.trim())
|
|
596
|
+
return;
|
|
597
|
+
const text = raw.replace(PASTE_RE, (_m, n) => this.pastes.get(Number(n)) ?? _m);
|
|
598
|
+
if (!this.promptOverride) {
|
|
599
|
+
this.history.push(raw);
|
|
600
|
+
if (this.opts.historyFile) {
|
|
601
|
+
try {
|
|
602
|
+
fs.mkdirSync(path.dirname(this.opts.historyFile), { recursive: true });
|
|
603
|
+
fs.appendFileSync(this.opts.historyFile, JSON.stringify(raw) + "\n");
|
|
604
|
+
}
|
|
605
|
+
catch { /* sem histórico */ }
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
this.buf = [];
|
|
609
|
+
this.cur = 0;
|
|
610
|
+
this.hIdx = -1;
|
|
611
|
+
this.menuSel = 0;
|
|
612
|
+
this.pastes.clear();
|
|
613
|
+
this.repaint();
|
|
614
|
+
this.onSubmit(text);
|
|
615
|
+
}
|
|
616
|
+
}
|
package/dist/ui/theme.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
const hex = (h) => {
|
|
2
|
+
const m = h.replace("#", "").match(/^([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i);
|
|
3
|
+
if (!m)
|
|
4
|
+
throw new Error(`cor inválida: ${h}`);
|
|
5
|
+
return [parseInt(m[1], 16), parseInt(m[2], 16), parseInt(m[3], 16)];
|
|
6
|
+
};
|
|
7
|
+
export const mix = (a, b, t) => a.map((v, i) => Math.round(v + (b[i] - v) * t));
|
|
8
|
+
/** Cor da marca. Troque com BRITO_COLOR=#rrggbb. */
|
|
9
|
+
const BASE = hex(process.env.BRITO_COLOR ?? "#8119bd");
|
|
10
|
+
export const useColor = !process.env.NO_COLOR && (!!process.env.FORCE_COLOR || !!process.stdout.isTTY);
|
|
11
|
+
const truecolor = /truecolor|24bit/i.test(process.env.COLORTERM ?? "");
|
|
12
|
+
const to256 = ([r, g, b]) => 16 + 36 * Math.round((r / 255) * 5) + 6 * Math.round((g / 255) * 5) + Math.round((b / 255) * 5);
|
|
13
|
+
const fgSeq = (c) => (truecolor ? `\x1b[38;2;${c[0]};${c[1]};${c[2]}m` : `\x1b[38;5;${to256(c)}m`);
|
|
14
|
+
export const RESET = "\x1b[0m";
|
|
15
|
+
const wrap = (open) => (s) => (useColor ? `${open}${s}${RESET}` : s);
|
|
16
|
+
const fg = (c) => wrap(fgSeq(c));
|
|
17
|
+
/** O roxo #8119bd é escuro: bordas e barras usam a cor pura; textos e rótulos usam o tom claro. */
|
|
18
|
+
export const palette = {
|
|
19
|
+
accent: BASE,
|
|
20
|
+
light: mix(BASE, [255, 255, 255], 0.45),
|
|
21
|
+
soft: mix(BASE, [255, 255, 255], 0.7),
|
|
22
|
+
muted: mix(BASE, [128, 128, 128], 0.55),
|
|
23
|
+
ok: hex("#4caf50"),
|
|
24
|
+
warn: hex("#ffa726"),
|
|
25
|
+
orange: hex("#ff7043"),
|
|
26
|
+
err: hex("#ef5350"),
|
|
27
|
+
};
|
|
28
|
+
export const c = {
|
|
29
|
+
accent: fg(palette.accent),
|
|
30
|
+
light: fg(palette.light),
|
|
31
|
+
soft: fg(palette.soft),
|
|
32
|
+
muted: fg(palette.muted),
|
|
33
|
+
ok: fg(palette.ok),
|
|
34
|
+
warn: fg(palette.warn),
|
|
35
|
+
err: fg(palette.err),
|
|
36
|
+
bold: wrap("\x1b[1m"),
|
|
37
|
+
dim: wrap("\x1b[2m"),
|
|
38
|
+
rgb: (col) => fg(col),
|
|
39
|
+
};
|
|
40
|
+
/** Estilos do markdown no texto do modelo: **negrito** roxo claro, *itálico* lilás, `código` laranja. */
|
|
41
|
+
export const BOLD = useColor ? `\x1b[1m${fgSeq(palette.light)}` : "";
|
|
42
|
+
export const ITALIC = useColor ? `\x1b[3m${fgSeq(palette.soft)}` : "";
|
|
43
|
+
export const CODE = useColor ? fgSeq(palette.orange) : "";
|
|
44
|
+
export const OFF = useColor ? RESET : "";
|
|
45
|
+
export const stripAnsi = (s) => s.replace(/\x1b\[[0-9;]*m/g, "");
|
|
46
|
+
export const cols = () => Math.max(40, process.stdout.columns || 80);
|
|
47
|
+
/** Corta uma string com ANSI para no máximo `n` colunas visíveis, preservando as cores. */
|
|
48
|
+
export function truncAnsi(s, n) {
|
|
49
|
+
let out = "", vis = 0;
|
|
50
|
+
for (let i = 0; i < s.length; i++) {
|
|
51
|
+
const m = s.slice(i).match(/^\x1b\[[0-9;]*m/);
|
|
52
|
+
if (m) {
|
|
53
|
+
out += m[0];
|
|
54
|
+
i += m[0].length - 1;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (vis >= n - 1 && stripAnsi(s).length > n)
|
|
58
|
+
return out + "…" + (useColor ? RESET : "");
|
|
59
|
+
out += s[i];
|
|
60
|
+
vis++;
|
|
61
|
+
}
|
|
62
|
+
return out;
|
|
63
|
+
}
|