primocode 11.2.0 → 11.3.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/README.md +2 -2
- package/lib/agentes-pessoais.js +5 -2
- package/lib/app-server/cli.js +3 -0
- package/lib/app-server/enderecos.js +43 -0
- package/lib/app-server/servidor.js +151 -31
- package/lib/avisos.js +1 -0
- package/lib/cli/sessao.js +174 -16
- package/lib/config/config.js +16 -1
- package/lib/diff.js +12 -2
- package/lib/motor/transcricao.js +57 -0
- package/lib/plugin/entregaveis/nucleo.js +2 -2
- package/lib/plugin/estudio/gosto.js +3 -0
- package/lib/plugin/estudio/kits/tela.html +3 -1
- package/lib/projetos-meta.js +20 -3
- package/lib/session/runner/act.js +23 -4
- package/lib/tool/tools.js +1 -0
- package/lib/tui/entrada.js +143 -18
- package/lib/tui/tela.js +312 -49
- package/lib/tui/ui.js +77 -3
- package/package.json +6 -2
- package/voz/primo_voz/transcrever.py +12 -0
package/lib/tui/tela.js
CHANGED
|
@@ -27,12 +27,26 @@
|
|
|
27
27
|
|
|
28
28
|
'use strict';
|
|
29
29
|
|
|
30
|
-
const { c, visibleWidth, truncate, logo } = require('./ui.js');
|
|
30
|
+
const { c, visibleWidth, truncate, logo, logoBloco, ampliar } = require('./ui.js');
|
|
31
31
|
|
|
32
32
|
const RESET = '\x1b[0m';
|
|
33
33
|
const FUNDO_LATERAL = '\x1b[48;5;234m';
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
/* A barra da direita. No OpenCode ela tem 42 colunas e só aparece de 120 para
|
|
35
|
+
cima. Aqui ela cabe em 100: de 100 a 119 colunas fica estreita (30, como no
|
|
36
|
+
artefato do estilo 1); de 120 para cima é a larga (42); abaixo de 100 some,
|
|
37
|
+
e a conversa fica com a tela inteira. Ctrl+X B liga e desliga, como lá. */
|
|
38
|
+
const LATERAL_W = 42;
|
|
39
|
+
const LATERAL_ESTREITA = 30;
|
|
40
|
+
const LATERAL_MIN = 100;
|
|
41
|
+
const LATERAL_LARGA = 120;
|
|
42
|
+
function larguraLateral(W, oculta) {
|
|
43
|
+
if (oculta) return 0;
|
|
44
|
+
/* Cresce com o terminal: 42 colunas até 159, 48 a partir de 160 e 56 a
|
|
45
|
+
partir de 200 (a conversa fica com o resto, que é sempre a maior parte). */
|
|
46
|
+
if (W >= 200) return 56;
|
|
47
|
+
if (W >= 160) return 48;
|
|
48
|
+
return W >= LATERAL_LARGA ? LATERAL_W : W >= LATERAL_MIN ? LATERAL_ESTREITA : 0;
|
|
49
|
+
}
|
|
36
50
|
const RECUO = 3; // quanto a resposta entra em relação ao painel do pedido
|
|
37
51
|
|
|
38
52
|
/* ── A conversa como texto ───────────────────────────────────────────────
|
|
@@ -57,11 +71,34 @@ function acrescentar(linhas, texto) {
|
|
|
57
71
|
if (k >= 0) linhas[linhas.length - 1] = p.slice(k + 1);
|
|
58
72
|
else linhas[linhas.length - 1] += p;
|
|
59
73
|
});
|
|
74
|
+
fecharAcao(linhas);
|
|
60
75
|
// Guarda as últimas 5.000: uma sessão longa não pode crescer sem fim.
|
|
61
76
|
if (linhas.length > 5000) linhas.splice(0, linhas.length - 5000);
|
|
62
77
|
return linhas;
|
|
63
78
|
}
|
|
64
79
|
|
|
80
|
+
/* A FERRAMENTA QUE TERMINOU. O motor escreve a ação com "●" quando ela começa
|
|
81
|
+
e o resultado ("⎿ ...") quando ela acaba. Na tela cheia a linha da ação vira
|
|
82
|
+
"✓" (verde) quando o resultado chega, e leva o "+N −M" do resultado, como as
|
|
83
|
+
linhas de ferramenta do OpenCode. Roda a cada texto novo e só mexe numa
|
|
84
|
+
linha que ainda esteja com "●". */
|
|
85
|
+
const SEM_COR = (t) => String(t).replace(/\x1b\[[0-9;]*m/g, '');
|
|
86
|
+
const VERDE = '\x1b[38;5;78m';
|
|
87
|
+
function fecharAcao(linhas) {
|
|
88
|
+
let n = linhas.length;
|
|
89
|
+
while (n > 0 && !/\S/.test(SEM_COR(linhas[n - 1]))) n--; // o texto acabou em "\n": a última linha vem vazia
|
|
90
|
+
if (n < 2 || !/^\s*⎿/.test(SEM_COR(linhas[n - 1]))) return;
|
|
91
|
+
for (let i = n - 2; i >= 0 && i >= n - 40; i--) {
|
|
92
|
+
const t = SEM_COR(linhas[i]);
|
|
93
|
+
if (/^\s*✓/.test(t)) return; // já fechada
|
|
94
|
+
if (!/^\s*●/.test(t)) continue;
|
|
95
|
+
const m = SEM_COR(linhas[n - 1]).match(/\+(\d+)\s+[−-](\d+)/);
|
|
96
|
+
linhas[i] = linhas[i].replace(/●/, VERDE + '✓' + RESET)
|
|
97
|
+
+ (m ? ' ' + c.ok('+' + m[1]) + ' ' + c.err('−' + m[2]) : '');
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
65
102
|
/**
|
|
66
103
|
* Quebra uma linha com cor em pedaços de no máximo `w` colunas: no ESPAÇO
|
|
67
104
|
* quando dá (palavra cortada ao meio é o que faz texto de terminal parecer
|
|
@@ -107,24 +144,113 @@ function caber(s, w, fundo) {
|
|
|
107
144
|
return fundo + String(s).split(RESET).join(RESET + fundo) + ' '.repeat(falta) + RESET;
|
|
108
145
|
}
|
|
109
146
|
|
|
147
|
+
/* ── Pedaços de linha com cor ────────────────────────────────────────── */
|
|
148
|
+
|
|
149
|
+
/** As colunas visíveis [de, ate) de uma linha com cor, religando a cor que estava ativa. */
|
|
150
|
+
function fatiar(linha, de, ate) {
|
|
151
|
+
let col = 0, ativo = '', out = '', dentro = false;
|
|
152
|
+
for (const m of String(linha).matchAll(/\x1b\[[0-9;]*m|[\s\S]/gu)) {
|
|
153
|
+
const t = m[0];
|
|
154
|
+
if (t.startsWith('\x1b[')) {
|
|
155
|
+
ativo = (t === RESET || t === '\x1b[m') ? '' : ativo + t;
|
|
156
|
+
if (dentro) out += t;
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
const w = visibleWidth(t);
|
|
160
|
+
if (col >= ate) break;
|
|
161
|
+
if (col >= de && col + w <= ate) { if (!dentro) { dentro = true; out += ativo; } out += t; }
|
|
162
|
+
col += w;
|
|
163
|
+
}
|
|
164
|
+
return out + (dentro && ativo ? RESET : '');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Põe `texto` (já colorido, `larg` colunas) por cima de `linha`, a partir da coluna `ini`. */
|
|
168
|
+
function sobrepor(linha, ini, texto, larg, W) {
|
|
169
|
+
const antes = caber(fatiar(linha, 0, ini), ini);
|
|
170
|
+
return antes + RESET + texto + RESET + fatiar(linha, ini + larg, W);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/* ── A cor do terminal ────────────────────────────────────────────────────
|
|
174
|
+
O desenho usa as 256 cores (funciona em qualquer terminal). Quando o terminal
|
|
175
|
+
diz que tem cor verdadeira, cada cor vira RGB, e a da marca é exatamente a
|
|
176
|
+
#22d3ee. NO_COLOR tira toda a cor (o item escolhido vira vídeo invertido). */
|
|
177
|
+
function modoDeCor(env = process.env) {
|
|
178
|
+
if (env.NO_COLOR !== undefined && env.NO_COLOR !== '') return 'none';
|
|
179
|
+
if (env.TERM === 'dumb') return 'none';
|
|
180
|
+
const ct = String(env.COLORTERM || '').toLowerCase();
|
|
181
|
+
if (ct.includes('truecolor') || ct.includes('24bit')) return 'true';
|
|
182
|
+
if (/^(iTerm\.app|vscode|WezTerm|ghostty|Hyper)$/.test(env.TERM_PROGRAM || '') || /direct|truecolor|kitty|ghostty|wezterm/.test(env.TERM || '')) return 'true';
|
|
183
|
+
return '256';
|
|
184
|
+
}
|
|
185
|
+
const NIVEL = [0, 95, 135, 175, 215, 255];
|
|
186
|
+
function rgb256(n) {
|
|
187
|
+
if (n < 16) return [[0, 0, 0], [128, 0, 0], [0, 128, 0], [128, 128, 0], [0, 0, 128], [128, 0, 128], [0, 128, 128], [192, 192, 192],
|
|
188
|
+
[128, 128, 128], [255, 0, 0], [0, 255, 0], [255, 255, 0], [0, 0, 255], [255, 0, 255], [0, 255, 255], [255, 255, 255]][n];
|
|
189
|
+
if (n >= 232) { const v = 8 + (n - 232) * 10; return [v, v, v]; }
|
|
190
|
+
const k = n - 16;
|
|
191
|
+
return [NIVEL[Math.floor(k / 36)], NIVEL[Math.floor(k / 6) % 6], NIVEL[k % 6]];
|
|
192
|
+
}
|
|
193
|
+
// As cores da marca e dos fundos de diff/permissão, exatas em cor verdadeira.
|
|
194
|
+
const EXATAS = {
|
|
195
|
+
45: [34, 211, 238], 31: [8, 145, 178], 254: [226, 232, 240], 247: [148, 163, 184], 243: [100, 116, 139],
|
|
196
|
+
214: [251, 191, 36], 204: [251, 113, 133],
|
|
197
|
+
'48:22': [13, 38, 24], '48:52': [43, 18, 22], '48:58': [42, 34, 12], '48:234': [20, 24, 33], '48:235': [29, 35, 49],
|
|
198
|
+
};
|
|
199
|
+
function adaptarCor(texto, modo) {
|
|
200
|
+
const t = String(texto);
|
|
201
|
+
if (modo === '256' || !modo) return t;
|
|
202
|
+
if (modo === 'none') {
|
|
203
|
+
return t.replace(/\x1b\[48;5;216m/g, '\x1b[7m')
|
|
204
|
+
.replace(/\x1b\[(?:38|48);5;\d+m/g, '').replace(/\x1b\[(?:3[0-7]|4[0-7]|9[0-7]|10[0-7])m/g, '');
|
|
205
|
+
}
|
|
206
|
+
return t.replace(/\x1b\[(38|48);5;(\d+)m/g, (_, tipo, n) => {
|
|
207
|
+
const [r, g, b] = EXATAS[tipo + ':' + n] || (tipo === '38' && EXATAS[n]) || rgb256(+n);
|
|
208
|
+
return `\x1b[${tipo};2;${r};${g};${b}m`;
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/* ── O painel colorido (pedido, permissão): barra grossa + fundo ─────────── */
|
|
213
|
+
const BARRA = { brand: '\x1b[38;5;45m', warn: '\x1b[38;5;214m', err: '\x1b[38;5;204m' };
|
|
214
|
+
const FUNDO = { brand: '\x1b[48;5;235m', warn: '\x1b[48;5;58m', err: '\x1b[48;5;52m' };
|
|
215
|
+
function painelCor(linhas, tipo, w) {
|
|
216
|
+
const barra = BARRA[tipo] || BARRA.brand, fundo = FUNDO[tipo] || FUNDO.brand;
|
|
217
|
+
const vazia = fundo + barra + '┃' + RESET + fundo + ' '.repeat(Math.max(0, w - 1)) + RESET;
|
|
218
|
+
const corpo = linhas.map((l) => {
|
|
219
|
+
const t = truncate(l, w - 4);
|
|
220
|
+
return fundo + barra + '┃' + RESET + fundo + ' ' + String(t).split(RESET).join(RESET + fundo)
|
|
221
|
+
+ ' '.repeat(Math.max(0, w - 3 - visibleWidth(t))) + RESET;
|
|
222
|
+
});
|
|
223
|
+
return [vazia, ...corpo, vazia];
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Barra de progresso "████░░░░" com `w` colunas. */
|
|
227
|
+
function barraDe(pct, w, cor) {
|
|
228
|
+
const cheio = Math.max(0, Math.min(w, Math.round((Math.max(0, Math.min(100, pct)) / 100) * w)));
|
|
229
|
+
return (cor || c.brand)('█'.repeat(cheio)) + c.dim('░'.repeat(w - cheio));
|
|
230
|
+
}
|
|
231
|
+
|
|
110
232
|
/* ── A barra da direita ──────────────────────────────────────────────── */
|
|
111
233
|
|
|
112
234
|
/**
|
|
113
|
-
*
|
|
235
|
+
* O que a barra mostra, de cima para baixo (a ordem e o espírito da do OpenCode):
|
|
236
|
+
* título da sessão, as seções (Contexto, Plano, Tarefas, Arquivos alterados,
|
|
237
|
+
* MCP/LSP), e no pé o Primo pequeno com o estado, a pasta e a versão.
|
|
238
|
+
* @param {object} lat { titulo, subtitulo, secoes: [{titulo, linhas}], pe: [], rodape: [] }
|
|
114
239
|
* @param {number} alto quantas linhas a tela tem
|
|
240
|
+
* @param {number} larg a largura da barra (30 ou 42)
|
|
115
241
|
*/
|
|
116
|
-
function lateral(lat, alto) {
|
|
117
|
-
const w =
|
|
242
|
+
function lateral(lat, alto, larg = LATERAL_W) {
|
|
243
|
+
const w = larg - 4; // 2 de margem de cada lado
|
|
118
244
|
const corpo = [''];
|
|
119
|
-
// O topo: o logo (animado enquanto trabalha) e o estado do Primo agora.
|
|
120
|
-
if (lat.topo && lat.topo.length) corpo.push(...lat.topo, '');
|
|
121
245
|
for (const l of quebrarComCor(c.bold(c.white(lat.titulo || 'Nova sessão')), w)) corpo.push(l);
|
|
246
|
+
if (lat.subtitulo) corpo.push(c.dim(truncate(lat.subtitulo, w)));
|
|
122
247
|
for (const s of (lat.secoes || [])) {
|
|
248
|
+
if (!s.linhas || !s.linhas.length) continue; // seção vazia não aparece
|
|
123
249
|
corpo.push('');
|
|
124
250
|
corpo.push(c.bold(c.white(s.titulo)));
|
|
125
251
|
for (const l of s.linhas) corpo.push(...quebrarComCor(l, w));
|
|
126
252
|
}
|
|
127
|
-
const rodape = (lat.rodape || [])
|
|
253
|
+
const rodape = [...(lat.pe && lat.pe.length ? [...lat.pe, ''] : []), ...(lat.rodape || [])];
|
|
128
254
|
rodape.push('');
|
|
129
255
|
const linhas = [];
|
|
130
256
|
for (let i = 0; i < alto; i++) {
|
|
@@ -132,13 +258,100 @@ function lateral(lat, alto) {
|
|
|
132
258
|
let l = '';
|
|
133
259
|
if (deBaixo < rodape.length) l = rodape[rodape.length - 1 - deBaixo];
|
|
134
260
|
else if (i < corpo.length) l = corpo[i];
|
|
135
|
-
linhas.push(caber(' ' + caber(l, w) + ' ',
|
|
261
|
+
linhas.push(caber(' ' + caber(l, w) + ' ', larg, FUNDO_LATERAL));
|
|
136
262
|
}
|
|
137
263
|
return linhas;
|
|
138
264
|
}
|
|
139
265
|
|
|
266
|
+
/* ── Diálogos e avisos (a paleta ctrl+p, modelo, agente, sessões, tema) ──── */
|
|
267
|
+
|
|
268
|
+
const SEL_FUNDO = '\x1b[48;5;45m\x1b[38;5;16m';
|
|
269
|
+
const DLG_FUNDO = '\x1b[48;5;235m';
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* O diálogo no meio da tela, como o `dialog-select` do OpenCode: título e "esc",
|
|
273
|
+
* a linha de busca, e a lista (agrupada por seção) com o item escolhido numa faixa.
|
|
274
|
+
* @param {object} d { titulo, filtro, itens: [{nome, desc, tecla, secao}], sel, vazio }
|
|
275
|
+
* @returns {{linhas: string[], largura: number, cursorLinha: number, cursorColuna: number}}
|
|
276
|
+
*/
|
|
277
|
+
function dialogo(d, W, H) {
|
|
278
|
+
const largura = Math.max(28, Math.min(Math.max(64, Math.round(W * 0.4)), 110, W - 4));
|
|
279
|
+
const dentro = largura - 4;
|
|
280
|
+
const pad = (s) => DLG_FUNDO + String(s).split(RESET).join(RESET + DLG_FUNDO) + ' '.repeat(Math.max(0, largura - visibleWidth(s))) + RESET;
|
|
281
|
+
const linha = (s) => pad(' ' + caber(s, dentro) + ' ');
|
|
282
|
+
const itens = d.itens || [];
|
|
283
|
+
const maxLista = Math.max(3, Math.min(Math.max(14, Math.round(H * 0.4)), H - 10));
|
|
284
|
+
// Linhas da lista: cabeçalho de seção quando ela muda, depois o item.
|
|
285
|
+
const lista = [];
|
|
286
|
+
let ultima = null;
|
|
287
|
+
itens.forEach((it, idx) => {
|
|
288
|
+
if (it.secao && it.secao !== ultima) { lista.push({ cab: it.secao }); ultima = it.secao; }
|
|
289
|
+
lista.push({ it, idx });
|
|
290
|
+
});
|
|
291
|
+
const posSel = Math.max(0, lista.findIndex((x) => x.idx === d.sel));
|
|
292
|
+
const topo = Math.max(0, Math.min(posSel - Math.floor(maxLista / 2), lista.length - maxLista));
|
|
293
|
+
const vis = lista.slice(topo, topo + maxLista);
|
|
294
|
+
|
|
295
|
+
const out = [];
|
|
296
|
+
out.push(pad(''));
|
|
297
|
+
const esc = c.dim('esc');
|
|
298
|
+
out.push(pad(' ' + c.bold(c.white(d.titulo || '')) + ' '.repeat(Math.max(1, dentro - visibleWidth(d.titulo || '') - 3)) + esc + ' '));
|
|
299
|
+
out.push(pad(''));
|
|
300
|
+
const buscar = (d.filtro ? c.white(d.filtro) : c.dim('Buscar'));
|
|
301
|
+
out.push(linha(buscar));
|
|
302
|
+
const cursorLinha = out.length - 1;
|
|
303
|
+
const cursorColuna = 2 + (d.filtro ? visibleWidth(d.filtro) : 0);
|
|
304
|
+
out.push(pad(''));
|
|
305
|
+
if (!itens.length) out.push(linha(c.dim(d.vazio || 'Nada achado')));
|
|
306
|
+
for (const x of vis) {
|
|
307
|
+
if (x.cab) { out.push(linha(c.bold(c.brand(x.cab)))); continue; }
|
|
308
|
+
const it = x.it;
|
|
309
|
+
const tecla = it.tecla || '';
|
|
310
|
+
const nomeW = Math.max(8, dentro - visibleWidth(tecla) - 2);
|
|
311
|
+
const esq = it.nome + (it.desc ? ' ' + it.desc : '');
|
|
312
|
+
if (x.idx === d.sel) {
|
|
313
|
+
const txt = ' ' + caber(truncate(it.nome + (it.desc ? ' ' + stripCor(it.desc) : ''), nomeW - 1), nomeW) + tecla + ' ';
|
|
314
|
+
out.push(pad(' ' + SEL_FUNDO + caber(txt, dentro) + RESET + DLG_FUNDO + ' '));
|
|
315
|
+
} else {
|
|
316
|
+
const corpo = c.white(it.nome) + (it.desc ? ' ' + c.dim(stripCor(it.desc)) : '');
|
|
317
|
+
const tx = truncate(corpo, nomeW);
|
|
318
|
+
out.push(linha(tx + ' '.repeat(Math.max(1, dentro - visibleWidth(tx) - visibleWidth(tecla))) + c.dim(tecla)));
|
|
319
|
+
}
|
|
320
|
+
void esq;
|
|
321
|
+
}
|
|
322
|
+
if (lista.length > maxLista) out.push(linha(c.dim(`↑↓ para ver mais (${itens.length} itens)`)));
|
|
323
|
+
out.push(pad(''));
|
|
324
|
+
return { linhas: out, largura, cursorLinha, cursorColuna };
|
|
325
|
+
}
|
|
326
|
+
const stripCor = SEM_COR;
|
|
327
|
+
|
|
328
|
+
/** O aviso que aparece uns segundos no canto de cima (toast do OpenCode). */
|
|
329
|
+
function toast(t, W) {
|
|
330
|
+
const tipo = t.tipo === 'erro' ? 'err' : t.tipo === 'aviso' ? 'warn' : 'brand';
|
|
331
|
+
const larg = Math.max(20, Math.min(Math.max(40, Math.round(W * 0.25)), 64, W - 4));
|
|
332
|
+
const texto = quebrarComCor(String(t.texto || ''), larg - 4).slice(0, 4);
|
|
333
|
+
const linhas = painelCor(texto, tipo, larg);
|
|
334
|
+
return { linhas, largura: larg };
|
|
335
|
+
}
|
|
336
|
+
|
|
140
337
|
/* ── O quadro inteiro ─────────────────────────────────────────────────── */
|
|
141
338
|
|
|
339
|
+
/** Os comandos que a home lista, com o atalho de cada um (o líder é o ctrl+x, como no OpenCode). */
|
|
340
|
+
const COMANDOS_HOME = [
|
|
341
|
+
['/clear', 'nova conversa', 'ctrl+x n'],
|
|
342
|
+
['/projects', 'abrir um projeto', 'ctrl+x p'],
|
|
343
|
+
['/estudio', 'criar vídeo, slides ou motion', 'ctrl+x s'],
|
|
344
|
+
['/agentes', 'ver quem está disponível', 'ctrl+x a'],
|
|
345
|
+
['/mode', 'trocar o modo ou o esforço', 'ctrl+x m'],
|
|
346
|
+
];
|
|
347
|
+
|
|
348
|
+
function linhaDeComando(cmd, desc, tecla, largura) {
|
|
349
|
+
const cw = 12;
|
|
350
|
+
const meio = largura - cw - visibleWidth(tecla) - 2;
|
|
351
|
+
return c.brand(cmd.padEnd(cw)) + c.white(truncate(desc, Math.max(4, meio))).padEnd(0)
|
|
352
|
+
+ ' '.repeat(Math.max(1, meio - visibleWidth(truncate(desc, Math.max(4, meio))))) + c.dim(tecla);
|
|
353
|
+
}
|
|
354
|
+
|
|
142
355
|
/**
|
|
143
356
|
* @param {object} e
|
|
144
357
|
* @param {number} e.colunas, e.linhas o tamanho do terminal
|
|
@@ -147,48 +360,96 @@ function lateral(lat, alto) {
|
|
|
147
360
|
* @param {boolean} e.home ainda não houve pedido: logo no meio
|
|
148
361
|
* @param {(colunas:number, centro:boolean)=>{linhas:string[], cursorLinha:number, cursorColuna:number}} e.caixa
|
|
149
362
|
* @param {object} e.lateral o conteúdo da barra da direita
|
|
150
|
-
* @param {
|
|
363
|
+
* @param {boolean} e.lateralOculta a pessoa desligou a barra (ctrl+x b)
|
|
364
|
+
* @param {string} e.versao, e.pasta, e.plano o rodapé da home
|
|
365
|
+
* @param {object} e.dialogo, e.toast o diálogo aberto e o aviso do canto
|
|
151
366
|
* @returns {{linhas: string[], cursor: {linha:number, coluna:number}, rolagemMax: number}}
|
|
152
367
|
*/
|
|
153
368
|
function quadro(e) {
|
|
154
369
|
const W = Math.max(30, e.colunas || 80);
|
|
155
370
|
const H = Math.max(10, e.linhas || 24);
|
|
371
|
+
const q = e.home ? quadroHome(e, W, H) : quadroSessao(e, W, H);
|
|
372
|
+
return aplicarSobreposicoes(q, e, W, H);
|
|
373
|
+
}
|
|
156
374
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
const
|
|
163
|
-
const
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
for (const l of lg) linhas.push(' '.repeat(Math.max(0, Math.floor((W - visibleWidth(l)) / 2))) + l);
|
|
168
|
-
linhas.push('', '');
|
|
169
|
-
const inicioCaixa = Math.max(0, linhas.length - menu);
|
|
170
|
-
linhas.length = inicioCaixa;
|
|
171
|
-
linhas.push(...caixa.linhas);
|
|
172
|
-
while (linhas.length < H - 1) linhas.push('');
|
|
173
|
-
linhas.length = H - 1;
|
|
174
|
-
const esq = ' ' + c.dim(e.pasta || '');
|
|
175
|
-
// Folga à direita: terminais como o Orca põem um botão flutuante no
|
|
176
|
-
// canto de baixo, e ele cobria a versão ("10.").
|
|
177
|
-
const dir = c.dim(e.versao || '') + ' ';
|
|
178
|
-
linhas.push(esq + ' '.repeat(Math.max(1, W - visibleWidth(esq) - visibleWidth(dir))) + dir);
|
|
179
|
-
return {
|
|
180
|
-
linhas: linhas.map((l) => caber(l, W)),
|
|
181
|
-
cursor: { linha: inicioCaixa + caixa.cursorLinha, coluna: caixa.cursorColuna },
|
|
182
|
-
rolagemMax: 0,
|
|
183
|
-
};
|
|
375
|
+
function aplicarSobreposicoes(q, e, W, H) {
|
|
376
|
+
let linhas = q.linhas.slice();
|
|
377
|
+
if (e.dialogo) {
|
|
378
|
+
// O fundo escurece (o "scrim" do OpenCode): sem cor, num cinza baixo, para o diálogo saltar.
|
|
379
|
+
linhas = linhas.map((l) => '\x1b[38;5;238m' + SEM_COR(l) + RESET);
|
|
380
|
+
const d = dialogo(e.dialogo, W, H);
|
|
381
|
+
const topo = Math.max(1, Math.min(Math.floor(H / 5), H - d.linhas.length - 1));
|
|
382
|
+
const ini = Math.floor((W - d.largura) / 2);
|
|
383
|
+
d.linhas.forEach((l, i) => { if (topo + i < H) linhas[topo + i] = sobrepor(linhas[topo + i], ini, l, d.largura, W); });
|
|
384
|
+
q = { ...q, cursor: { linha: topo + d.cursorLinha, coluna: ini + d.cursorColuna } };
|
|
184
385
|
}
|
|
386
|
+
if (e.toast && e.toast.texto) {
|
|
387
|
+
const t = toast(e.toast, W);
|
|
388
|
+
const ini = W - t.largura - 2;
|
|
389
|
+
t.linhas.forEach((l, i) => { if (1 + i < H) linhas[1 + i] = sobrepor(linhas[1 + i], ini, l, t.largura, W); });
|
|
390
|
+
}
|
|
391
|
+
return { ...q, linhas };
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function quadroHome(e, W, H) {
|
|
395
|
+
const caixa = e.caixa(W, true);
|
|
396
|
+
/* Terminal grande (160x45 para cima): o Primo e o logo em 2x, pixel a pixel. */
|
|
397
|
+
const grande = W >= 160 && H >= 45;
|
|
398
|
+
const dobrar = (a) => (grande ? ampliar(a) : a);
|
|
399
|
+
const art = dobrar(e.logo && e.logo.length ? e.logo : (W >= 50 ? logoBloco() : [c.bold(c.white('primocode'))]));
|
|
400
|
+
/* O painel fica PARADO no lugar dele: o menu, quando abre, cobre o que está
|
|
401
|
+
em cima (o logo), em vez de empurrar tudo para cima. */
|
|
402
|
+
const menu = Math.max(0, caixa.linhas.findIndex((l) => l.includes('┃')));
|
|
403
|
+
const nucleo = caixa.linhas.slice(menu);
|
|
404
|
+
const dica = ' '.repeat(0) + c.white('tab') + ' ' + c.dim('troca o modo') + ' ' + c.white('ctrl+p') + ' ' + c.dim('comandos');
|
|
405
|
+
const cmds = e.comandosHome || COMANDOS_HOME;
|
|
406
|
+
// A lista acompanha a caixa (que cresce com o terminal): mesma borda, só que mais estreita.
|
|
407
|
+
const larguraBox = Math.max(0, ...caixa.linhas.filter((l) => l.includes('┃')).map((l) => visibleWidth(l) - (SEM_COR(l).match(/^ */)[0].length)));
|
|
408
|
+
const larguraCmd = Math.min(W >= 160 ? Math.max(52, larguraBox - 8) : 52, W - 6);
|
|
409
|
+
// Quanto cabe: primeiro some a lista de comandos, depois o Primo.
|
|
410
|
+
const montar = (comLista, comPrimo) => {
|
|
411
|
+
const arte = comPrimo ? art : dobrar(e.logoSimples || logoBloco());
|
|
412
|
+
const topo = [...arte, '', c.dim('v' + String(e.versao || '').replace(/^v/, '')), ''];
|
|
413
|
+
const base = [dica];
|
|
414
|
+
if (comLista) base.push('', ...cmds.map(([a, b, k]) => ' '.repeat(Math.floor((W - larguraCmd) / 2)) + linhaDeComando(a, b, k, larguraCmd)));
|
|
415
|
+
return { topo, base };
|
|
416
|
+
};
|
|
417
|
+
let m = montar(true, true);
|
|
418
|
+
const altoDe = (x) => x.topo.length + nucleo.length + 1 + x.base.length;
|
|
419
|
+
if (altoDe(m) > H - 2) m = montar(false, true);
|
|
420
|
+
if (altoDe(m) > H - 2) m = montar(false, false);
|
|
421
|
+
const total = altoDe(m);
|
|
422
|
+
const inicio = Math.max(0, Math.floor((H - 1 - total) / 2));
|
|
423
|
+
const linhas = new Array(H - 1).fill('');
|
|
424
|
+
const centrar = (l) => ' '.repeat(Math.max(0, Math.floor((W - visibleWidth(l)) / 2))) + l;
|
|
425
|
+
m.topo.forEach((l, i) => { if (inicio + i < H - 1) linhas[inicio + i] = centrar(l); });
|
|
426
|
+
const linhaCaixa = inicio + m.topo.length;
|
|
427
|
+
// o menu aberto sobe por cima do logo (as linhas antes do painel)
|
|
428
|
+
caixa.linhas.slice(0, menu).forEach((l, i) => { const k = linhaCaixa - menu + i; if (k >= 0 && k < H - 1) linhas[k] = l; });
|
|
429
|
+
nucleo.forEach((l, i) => { if (linhaCaixa + i < H - 1) linhas[linhaCaixa + i] = l; });
|
|
430
|
+
const larguraBase = linhaCaixa + nucleo.length + 1;
|
|
431
|
+
m.base.forEach((l, i) => {
|
|
432
|
+
if (larguraBase + i < H - 1) linhas[larguraBase + i] = l.startsWith(' ') ? l : centrar(l);
|
|
433
|
+
});
|
|
434
|
+
const esq = ' ' + c.dim(e.pasta || '');
|
|
435
|
+
// Folga à direita: terminais como o Orca põem um botão flutuante no canto de baixo.
|
|
436
|
+
const dir = c.dim((e.plano ? e.plano + ' · ' : '') + (e.versao || '')) + ' ';
|
|
437
|
+
linhas.push(esq + ' '.repeat(Math.max(1, W - visibleWidth(esq) - visibleWidth(dir))) + dir);
|
|
438
|
+
return {
|
|
439
|
+
linhas: linhas.map((l) => caber(l, W)),
|
|
440
|
+
cursor: { linha: linhaCaixa + caixa.cursorLinha - menu, coluna: caixa.cursorColuna },
|
|
441
|
+
rolagemMax: 0,
|
|
442
|
+
};
|
|
443
|
+
}
|
|
185
444
|
|
|
445
|
+
function quadroSessao(e, W, H) {
|
|
186
446
|
/* O PAINEL de um comando que desenha a própria tela (o slider do
|
|
187
447
|
/effort, o marketplace): fica no pé da conversa, logo acima da caixa,
|
|
188
448
|
por cima do que estiver ali — a tela cheia nunca sai do ar. */
|
|
189
449
|
const painel = (e.painel ? String(e.painel).split('\n') : []);
|
|
190
|
-
const
|
|
191
|
-
const
|
|
450
|
+
const lw = larguraLateral(W, e.lateralOculta);
|
|
451
|
+
const comLateral = lw > 0;
|
|
452
|
+
const mainW = comLateral ? W - lw : W;
|
|
192
453
|
/* Uma coluna de margem dos dois lados, para a conversa e para a caixa: o
|
|
193
454
|
pedido mandado (um painel) e a caixa de digitar ficam na MESMA borda. */
|
|
194
455
|
const caixa0 = e.caixa(mainW - 1, false);
|
|
@@ -197,13 +458,12 @@ function quadro(e) {
|
|
|
197
458
|
|
|
198
459
|
/* A conversa, quebrada na largura da coluna. Os PAINÉIS (o pedido que se
|
|
199
460
|
mandou) ficam na borda da caixa; o resto — a resposta, as ações, o
|
|
200
|
-
fecho "▣" — entra RECUADO, como no OpenCode.
|
|
201
|
-
colado na borda e um bloco não se separava do outro. */
|
|
461
|
+
fecho "▣" — entra RECUADO, como no OpenCode. */
|
|
202
462
|
const visiveis = [];
|
|
203
463
|
for (const l of (e.conversa || [])) {
|
|
204
|
-
const
|
|
205
|
-
const margem =
|
|
206
|
-
for (const p of quebrarComCor(l,
|
|
464
|
+
const ehPainel = /^(\x1b\[[0-9;]*m)*┃/.test(l);
|
|
465
|
+
const margem = ehPainel ? ' ' : ' '.repeat(RECUO + 1);
|
|
466
|
+
for (const p of quebrarComCor(l, ehPainel ? mainW - 2 : mainW - 2 - RECUO)) visiveis.push(margem + p);
|
|
207
467
|
}
|
|
208
468
|
while (visiveis.length && !temTexto(visiveis[visiveis.length - 1])) visiveis.pop();
|
|
209
469
|
if (visiveis.length) visiveis.push(''); // um respiro entre a conversa e a caixa
|
|
@@ -225,7 +485,7 @@ function quadro(e) {
|
|
|
225
485
|
}
|
|
226
486
|
|
|
227
487
|
const esquerda = [...conversa, ...caixa.linhas].map((l) => caber(l, mainW));
|
|
228
|
-
const direita = comLateral ? lateral(e.lateral || {}, H) : null;
|
|
488
|
+
const direita = comLateral ? lateral(e.lateral || {}, H, lw) : null;
|
|
229
489
|
const linhas = esquerda.map((l, i) => (direita ? l + direita[i] : l));
|
|
230
490
|
return {
|
|
231
491
|
linhas,
|
|
@@ -237,9 +497,12 @@ function quadro(e) {
|
|
|
237
497
|
function temTexto(l) { return visibleWidth(l) > 0 && /\S/.test(l.replace(/\x1b\[[0-9;]*m/g, '')); }
|
|
238
498
|
|
|
239
499
|
/** A largura que o texto da conversa tem, numa tela de `colunas`. */
|
|
240
|
-
function larguraDaConversa(colunas) {
|
|
500
|
+
function larguraDaConversa(colunas, oculta) {
|
|
241
501
|
const W = Math.max(30, colunas || 80);
|
|
242
|
-
return (W
|
|
502
|
+
return (W - larguraLateral(W, oculta)) - 2 - RECUO;
|
|
243
503
|
}
|
|
244
504
|
|
|
245
|
-
module.exports = {
|
|
505
|
+
module.exports = {
|
|
506
|
+
RECUO, larguraDaConversa, quadro, acrescentar, quebrarComCor, lateral, caber, LATERAL_W, LATERAL_ESTREITA, LATERAL_MIN, LATERAL_LARGA,
|
|
507
|
+
larguraLateral, dialogo, toast, painelCor, barraDe, fatiar, sobrepor, modoDeCor, adaptarCor, COMANDOS_HOME, fecharAcao,
|
|
508
|
+
};
|
package/lib/tui/ui.js
CHANGED
|
@@ -36,6 +36,20 @@ const PALETTE = {
|
|
|
36
36
|
reset: '\x1b[0m',
|
|
37
37
|
};
|
|
38
38
|
|
|
39
|
+
/* NO_COLOR (https://no-color.org): sem nenhuma cor. Negrito e reset ficam. */
|
|
40
|
+
if (process.env.NO_COLOR !== undefined && process.env.NO_COLOR !== '') {
|
|
41
|
+
for (const k of Object.keys(PALETTE)) if (k !== 'bold' && k !== 'reset') PALETTE[k] = '';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/* O que a barra da direita da tela cheia mostra e que vem do trabalho do agente:
|
|
45
|
+
as tarefas (write_todos) e os arquivos alterados, com +N −M. O act.js preenche. */
|
|
46
|
+
const estadoSessao = { todos: [], arquivos: new Map() };
|
|
47
|
+
function registrarArquivo(caminho, mais, menos) {
|
|
48
|
+
if (!caminho) return;
|
|
49
|
+
const a = estadoSessao.arquivos.get(caminho) || { mais: 0, menos: 0 };
|
|
50
|
+
estadoSessao.arquivos.set(caminho, { mais: a.mais + mais, menos: a.menos + menos });
|
|
51
|
+
}
|
|
52
|
+
|
|
39
53
|
const c = {
|
|
40
54
|
...PALETTE,
|
|
41
55
|
brand: (s) => `${PALETTE.brand}${s}${PALETTE.reset}`,
|
|
@@ -351,6 +365,66 @@ function letreiro(palavra) {
|
|
|
351
365
|
return out;
|
|
352
366
|
}
|
|
353
367
|
|
|
368
|
+
|
|
369
|
+
/* ── O logo em letras de bloco de 3 linhas (a home do estilo 2) ─────────
|
|
370
|
+
"primo" no tom do texto e "code" na cor da marca, como no artefato
|
|
371
|
+
.refazer/cli-alvo.html. Desenho próprio, não de fonte nenhuma. */
|
|
372
|
+
const BLOCOS = {
|
|
373
|
+
p: ['█▀▀█', '█▄▄█', '█ '], r: ['█▀▀▀', '█ ', '▀ '], i: ['▀█▀', ' █ ', '▀▀▀'],
|
|
374
|
+
m: ['█▀▄▀█', '█ ▀ █', '▀ ▀'], o: ['█▀▀█', '█ █', '▀▀▀▀'], c: ['█▀▀▀', '█ ', '▀▀▀▀'],
|
|
375
|
+
d: ['█▀▀▄', '█ █', '▀▀▀ '], e: ['█▀▀▀', '█▀▀ ', '▀▀▀▀'],
|
|
376
|
+
};
|
|
377
|
+
const palavraBloco = (w) => [0, 1, 2].map((j) => [...w].map((ch) => BLOCOS[ch][j]).join(' '));
|
|
378
|
+
function logoBloco() {
|
|
379
|
+
const a = palavraBloco('primo'), b = palavraBloco('code');
|
|
380
|
+
return a.map((l, i) => `\x1b[38;5;254m${l}${PALETTE.reset} ${PALETTE.brand}${b[i]}${PALETTE.reset}`);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/* Escala 2x, pixel a pixel: cada célula vira 2x2 células e cada quarto de
|
|
384
|
+
célula (os 4 pixels que ▀▄▌▐▛▜▙▟ desenham) vira UMA célula cheia, na cor
|
|
385
|
+
da célula de origem. É o mesmo desenho, maior, sem redesenhar nada. */
|
|
386
|
+
const QUARTOS_BITS = { '▀': 0b1100, '▄': 0b0011, '█': 0b1111, '▌': 0b1010, '▐': 0b0101, '▘': 0b1000, '▝': 0b0100, '▖': 0b0010, '▗': 0b0001,
|
|
387
|
+
'▛': 0b1110, '▜': 0b1101, '▙': 0b1011, '▟': 0b0111, '▚': 0b1001, '▞': 0b0110 };
|
|
388
|
+
function ampliar(linhas) {
|
|
389
|
+
const out = [];
|
|
390
|
+
for (const linha of linhas) {
|
|
391
|
+
let cor = '';
|
|
392
|
+
const cel = [];
|
|
393
|
+
for (const m of String(linha).matchAll(/(\x1b\[[0-9;]*m)|([\s\S])/gu)) {
|
|
394
|
+
if (m[1]) { cor = m[1] === '\x1b[0m' ? '' : m[1]; continue; }
|
|
395
|
+
cel.push({ ch: m[2], cor });
|
|
396
|
+
}
|
|
397
|
+
const topo = [], base = [];
|
|
398
|
+
for (const { ch, cor: k } of cel) {
|
|
399
|
+
const q = ch === ' ' ? 0 : (QUARTOS_BITS[ch] !== undefined ? QUARTOS_BITS[ch] : 0b1111);
|
|
400
|
+
const px = (b) => (q & b ? k + '█' + (k ? '\x1b[0m' : '') : ' ');
|
|
401
|
+
topo.push(px(8) + px(4)); base.push(px(2) + px(1));
|
|
402
|
+
}
|
|
403
|
+
out.push(topo.join(''), base.join(''));
|
|
404
|
+
}
|
|
405
|
+
return out;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/* TEMAS: o escuro é o padrão, com a cor da marca (#22d3ee). Os outros só
|
|
409
|
+
trocam a cor de destaque: `c.brand` lê o PALETTE na hora de pintar. */
|
|
410
|
+
const TEMAS = {
|
|
411
|
+
marca: { nome: 'Marca (ciano)', brand: 45, brandDim: 31 },
|
|
412
|
+
ambar: { nome: 'Âmbar', brand: 214, brandDim: 172 },
|
|
413
|
+
violeta: { nome: 'Violeta', brand: 141, brandDim: 98 },
|
|
414
|
+
verde: { nome: 'Verde', brand: 78, brandDim: 35 },
|
|
415
|
+
rosa: { nome: 'Rosa', brand: 211, brandDim: 168 },
|
|
416
|
+
};
|
|
417
|
+
let temaAtual = 'marca';
|
|
418
|
+
function definirTema(id) {
|
|
419
|
+
const t = TEMAS[id];
|
|
420
|
+
if (!t) return false;
|
|
421
|
+
PALETTE.brand = `\x1b[38;5;${t.brand}m`; PALETTE.brandDim = `\x1b[38;5;${t.brandDim}m`;
|
|
422
|
+
PALETTE.ok = PALETTE.brand; PALETTE.cyan = PALETTE.brand;
|
|
423
|
+
temaAtual = id;
|
|
424
|
+
return true;
|
|
425
|
+
}
|
|
426
|
+
const temaDoMomento = () => temaAtual;
|
|
427
|
+
|
|
354
428
|
/** O logo: "primo" apagado e "code" aceso, lado a lado. */
|
|
355
429
|
function logo() {
|
|
356
430
|
const a = letreiro('primo'), b = letreiro('code');
|
|
@@ -363,7 +437,7 @@ const LOGO_W = visibleWidth(logo()[0]);
|
|
|
363
437
|
home ele cresce 2×: cada quarto de bloco (▘ ▝ ▖ ▗) vira um bloco cheio, e
|
|
364
438
|
cada bloco cheio vira 2×2. Ficam 6 linhas × 18 colunas, com a cor de cada
|
|
365
439
|
pedaço preservada — o mesmo desenho, não um desenho novo. */
|
|
366
|
-
const
|
|
440
|
+
const QUARTOS_AMP = {
|
|
367
441
|
' ': [0, 0, 0, 0], '█': [1, 1, 1, 1], '▀': [1, 1, 0, 0], '▄': [0, 0, 1, 1],
|
|
368
442
|
'▌': [1, 0, 1, 0], '▐': [0, 1, 0, 1], '▘': [1, 0, 0, 0], '▝': [0, 1, 0, 0],
|
|
369
443
|
'▖': [0, 0, 1, 0], '▗': [0, 0, 0, 1], '▛': [1, 1, 1, 0], '▜': [1, 1, 0, 1],
|
|
@@ -381,7 +455,7 @@ function mascoteGrande(style) {
|
|
|
381
455
|
}
|
|
382
456
|
const cima = [], baixo = [];
|
|
383
457
|
for (const [ch, cr] of cels) {
|
|
384
|
-
const q =
|
|
458
|
+
const q = QUARTOS_AMP[ch] || (ch === ' ' ? QUARTOS_AMP[' '] : QUARTOS_AMP['█']);
|
|
385
459
|
const bloco = (b) => (b ? `${cr}█${PALETTE.reset}` : ' ');
|
|
386
460
|
cima.push(bloco(q[0]) + bloco(q[1]));
|
|
387
461
|
baixo.push(bloco(q[2]) + bloco(q[3]));
|
|
@@ -760,4 +834,4 @@ function friendlyError(raw) {
|
|
|
760
834
|
|
|
761
835
|
module.exports = {
|
|
762
836
|
extraDoSpinner,
|
|
763
|
-
titulo, agora, c, logo, logoComMascote, mascoteGrande, logoAnimado, letreiro, convite, EXEMPLOS, mascot, MASCOTES, resolverMascote, mascoteInfo, galeriaMascotes, reacao, welcomeBox, Spinner, PENSANDO, actionHeader, friendlyError, visibleWidth, stripAnsi, truncate, padRight };
|
|
837
|
+
estadoSessao, registrarArquivo, logoBloco, ampliar, TEMAS, definirTema, temaDoMomento, titulo, agora, c, logo, logoComMascote, mascoteGrande, logoAnimado, letreiro, convite, EXEMPLOS, mascot, MASCOTES, resolverMascote, mascoteInfo, galeriaMascotes, reacao, welcomeBox, Spinner, PENSANDO, actionHeader, friendlyError, visibleWidth, stripAnsi, truncate, padRight };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "primocode",
|
|
3
|
-
"version": "11.
|
|
3
|
+
"version": "11.3.0",
|
|
4
4
|
"description": "PrimoCode — agente de engenharia com IA e cursor próprio. Requer conta Conecta Primo AI (Premium ou Super). Cria arquivos, roda comandos, controla navegador e desktop: abre apps, clica em botões e ícones pelo nome, digita e usa atalhos.",
|
|
5
5
|
"main": "bin/primocode.js",
|
|
6
6
|
"bin": {
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"postinstall": "node lib/preparar.js || true",
|
|
12
12
|
"test:server": "cd server && npm test",
|
|
13
13
|
"test": "node scripts/testes.js",
|
|
14
|
-
"test:app": "node test/app-fluxo.test.mjs && node test/app-primeira.test.mjs && node test/app-entrar.test.mjs",
|
|
14
|
+
"test:app": "node test/app-fluxo.test.mjs && node test/app-primeira.test.mjs && node test/app-entrar.test.mjs && node test/app-tela-estudio.test.mjs && node test/app-celular-telas.test.mjs",
|
|
15
15
|
"app:build": "cd app && npm run dist",
|
|
16
16
|
"app:dev": "cd app && npm start",
|
|
17
17
|
"mobile:build": "cd mobile && npm run build"
|
|
@@ -55,5 +55,9 @@
|
|
|
55
55
|
],
|
|
56
56
|
"primocode": {
|
|
57
57
|
"servidorMinimo": "8.12.0"
|
|
58
|
+
},
|
|
59
|
+
"devDependencies": {
|
|
60
|
+
"pixelmatch": "^5.3.0",
|
|
61
|
+
"pngjs": "^7.0.0"
|
|
58
62
|
}
|
|
59
63
|
}
|
|
@@ -200,6 +200,18 @@ def main() -> int:
|
|
|
200
200
|
return 1
|
|
201
201
|
print(json.dumps({"ok": True, "palavras": ws}, ensure_ascii=False))
|
|
202
202
|
return 0
|
|
203
|
+
if acao == "arquivo":
|
|
204
|
+
# `... arquivo <wav> [idioma]`: a fala de um arquivo vira texto (o app desktop grava no microfone e manda para cá).
|
|
205
|
+
if not disponivel():
|
|
206
|
+
print(json.dumps({"ok": False, "error": "o faster-whisper não está instalado"}))
|
|
207
|
+
return 1
|
|
208
|
+
try:
|
|
209
|
+
texto = transcrever(Path(sys.argv[2]), sys.argv[3] if len(sys.argv) > 3 else "pt")
|
|
210
|
+
except Exception as e:
|
|
211
|
+
print(json.dumps({"ok": False, "error": str(e)}))
|
|
212
|
+
return 1
|
|
213
|
+
print(json.dumps({"ok": True, "texto": texto}, ensure_ascii=False))
|
|
214
|
+
return 0
|
|
203
215
|
if acao != "baixar":
|
|
204
216
|
print(json.dumps({"ok": False, "error": f"ação desconhecida: {acao}"}))
|
|
205
217
|
return 2
|