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.
@@ -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
+ }
@@ -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
+ }