primocode 9.2.3 → 9.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/bin/primocode.js +67 -2
- package/lib/entrada.js +27 -4
- package/lib/skills.js +83 -7
- package/package.json +1 -1
- package/skills/pagina-que-converte/SKILL.md +65 -18
- package/voz/primo_voz/orbe.py +189 -156
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# PrimoCode v9.2.
|
|
1
|
+
# PrimoCode v9.2.4
|
|
2
2
|
|
|
3
3
|
Agente de engenharia brasileiro para o terminal, no estilo Claude Code.
|
|
4
4
|
Cria arquivos de verdade, roda comandos, **controla o navegador e o desktop** —
|
|
@@ -9,7 +9,7 @@ OpenRouter, e cada apelido (`top`/`main`/`fast`) é uma **corrente**: se um mode
|
|
|
9
9
|
está no limite de pedidos, o próximo atende. Você não vê o erro, e não paga nada.
|
|
10
10
|
|
|
11
11
|
```
|
|
12
|
-
▐▛███▜▌ Primo Code v9.2.
|
|
12
|
+
▐▛███▜▌ Primo Code v9.2.4
|
|
13
13
|
▝▜█████▛▘ Bem-vindo de volta, Joel
|
|
14
14
|
▘▘ ▝▝ ~/primocode · /dir <pasta> muda
|
|
15
15
|
```
|
package/bin/primocode.js
CHANGED
|
@@ -151,6 +151,7 @@ const SLASH_COMANDOS = [
|
|
|
151
151
|
['/account', 'sua conta, plano e computadores conectados'],
|
|
152
152
|
['/chats', 'suas conversas salvas, as mesmas do app e do celular'],
|
|
153
153
|
['/logout', 'desconecta este computador da conta'],
|
|
154
|
+
['!comando', 'roda direto no shell, sem passar pelo agente (o campo fica âmbar)'],
|
|
154
155
|
['/voice', 'fica ouvindo em segundo plano — diga "ei, primo" e a orb aparece'],
|
|
155
156
|
['/voice parar', 'desliga o modo voz e fecha o microfone'],
|
|
156
157
|
['/skills', 'o que ele sabe fazer bem — ligue e desligue'],
|
|
@@ -1029,6 +1030,62 @@ async function runChat(promptForModel, userTextForMemory) {
|
|
|
1029
1030
|
|
|
1030
1031
|
function isSlash(text) { return text.trim().startsWith('/'); }
|
|
1031
1032
|
|
|
1033
|
+
/* ── "!comando" RODA NO SHELL ────────────────────────────────────────────
|
|
1034
|
+
*
|
|
1035
|
+
* "quando eu mande uma mensagem que comece com ! ele rode em bash"
|
|
1036
|
+
*
|
|
1037
|
+
* Por que não mandar ao agente e deixar ele chamar run_shell: porque são
|
|
1038
|
+
* coisas diferentes. Pedir ao agente é dizer o QUE se quer e deixá-lo
|
|
1039
|
+
* decidir; o "!" é dizer o COMANDO. Quem digita `!git status` não quer que
|
|
1040
|
+
* alguém interprete a intenção — quer a saída do git status, agora, sem
|
|
1041
|
+
* volta ao modelo, sem gastar cota e sem o risco de o agente achar que
|
|
1042
|
+
* entendeu melhor.
|
|
1043
|
+
*
|
|
1044
|
+
* A saída sai CRUA, como no terminal: ela não passa pelo desenhador de
|
|
1045
|
+
* Markdown. Saída de comando tem alinhamento próprio (colunas do `ls`,
|
|
1046
|
+
* tabela do `docker ps`), e reformatá-la seria estragá-la.
|
|
1047
|
+
*/
|
|
1048
|
+
function ehBash(text) { return /^\s*!/.test(String(text || '')); }
|
|
1049
|
+
|
|
1050
|
+
async function rodarBash(text) {
|
|
1051
|
+
const comando = String(text).replace(/^\s*!/, '').trim();
|
|
1052
|
+
if (!comando) return;
|
|
1053
|
+
|
|
1054
|
+
const pasta = state.workDir || process.cwd();
|
|
1055
|
+
console.log('');
|
|
1056
|
+
console.log(' ' + c.warn('$ ') + c.white(comando));
|
|
1057
|
+
|
|
1058
|
+
const inicio = Date.now();
|
|
1059
|
+
const r = await tools.execute('run_shell', { command: comando, timeout: 600000 },
|
|
1060
|
+
{ projectDir: pasta });
|
|
1061
|
+
const segundos = ((Date.now() - inicio) / 1000).toFixed(1);
|
|
1062
|
+
|
|
1063
|
+
/* Crua, e inteira. `run_shell` corta em 10 MB, que é o teto do buffer;
|
|
1064
|
+
cortar mais aqui esconderia justamente a linha do erro, que costuma
|
|
1065
|
+
estar no fim. */
|
|
1066
|
+
const saida = [r.stdout, r.stderr].filter((x) => String(x || '').trim()).join('\n');
|
|
1067
|
+
if (saida) console.log(saida.replace(/\n$/, ''));
|
|
1068
|
+
|
|
1069
|
+
if (r.ok) {
|
|
1070
|
+
console.log(' ' + c.dim(`ok · ${segundos}s`));
|
|
1071
|
+
} else if (r.killed) {
|
|
1072
|
+
console.log(' ' + c.warn(`interrompido depois de ${segundos}s`));
|
|
1073
|
+
} else {
|
|
1074
|
+
console.log(' ' + c.warn(`saiu com ${r.code} · ${segundos}s`));
|
|
1075
|
+
}
|
|
1076
|
+
console.log('');
|
|
1077
|
+
|
|
1078
|
+
/* O agente FICA SABENDO, e isto é o que torna o "!" útil dentro de uma
|
|
1079
|
+
conversa: rodar o teste e depois perguntar "por que falhou?" só
|
|
1080
|
+
funciona se ele tiver visto a saída. Sem isto seriam dois mundos
|
|
1081
|
+
separados na mesma tela. */
|
|
1082
|
+
state.history.push({ role: 'user', content: `Rodei no shell: ${comando}` });
|
|
1083
|
+
state.history.push({
|
|
1084
|
+
role: 'assistant',
|
|
1085
|
+
content: `Saída (código ${r.code ?? 0}):\n${saida.slice(0, 4000) || '(vazia)'}`,
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1032
1089
|
/* ── OS NOMES ANTIGOS ────────────────────────────────────────────────────
|
|
1033
1090
|
* Os comandos passaram a ser todos em inglês. Metade já era; a outra metade
|
|
1034
1091
|
* estava em português, e a mistura era o que a especificação chamou de
|
|
@@ -1430,7 +1487,14 @@ async function handleSlash(text) {
|
|
|
1430
1487
|
}
|
|
1431
1488
|
|
|
1432
1489
|
const pronto = vozModo.conferir();
|
|
1433
|
-
|
|
1490
|
+
/* `.length`, e não a lista: [] é VERDADEIRO em JavaScript.
|
|
1491
|
+
Quando só falta o tkinter — que o pip não instala, porque ele
|
|
1492
|
+
vem com o Python ou num pacote do sistema — a lista de
|
|
1493
|
+
instaláveis volta VAZIA. Sem o `.length`, este ramo era pulado,
|
|
1494
|
+
o "Instalar agora?" aparecia, e o pip era chamado sem pacote
|
|
1495
|
+
nenhum: "You must give at least one requirement to install".
|
|
1496
|
+
Visto na máquina do dono. */
|
|
1497
|
+
if (!pronto.ok && !(pronto.instalavel && pronto.instalavel.length)) {
|
|
1434
1498
|
console.log(c.err(` ${pronto.error}`));
|
|
1435
1499
|
if (pronto.comoResolver) console.log(c.muted(` ${pronto.comoResolver}`));
|
|
1436
1500
|
break;
|
|
@@ -2070,7 +2134,8 @@ async function main() {
|
|
|
2070
2134
|
const text = queue.shift().trim();
|
|
2071
2135
|
if (!text) continue;
|
|
2072
2136
|
try {
|
|
2073
|
-
if (
|
|
2137
|
+
if (ehBash(text)) await rodarBash(text);
|
|
2138
|
+
else if (isSlash(text)) await handleSlash(text);
|
|
2074
2139
|
else await sendPrompt(text);
|
|
2075
2140
|
} catch (e) {
|
|
2076
2141
|
fala.problema(e);
|
package/lib/entrada.js
CHANGED
|
@@ -115,16 +115,29 @@ function montar(e) {
|
|
|
115
115
|
toda linha e a preencher o resto com espaço, e é ela que faz o texto
|
|
116
116
|
colado parecer desalinhado quando o terminal é estreito. Sem as laterais
|
|
117
117
|
o campo ganha as colunas de volta e a emenda some. */
|
|
118
|
+
/* MODO BASH: a linha que começa com "!" roda no shell, e a caixa INTEIRA
|
|
119
|
+
muda de cor para avisar.
|
|
120
|
+
|
|
121
|
+
O aviso é o ponto. Um comando de shell faz coisa irreversível, e a
|
|
122
|
+
diferença entre "pedir ao agente" e "rodar direto" não pode depender de
|
|
123
|
+
a pessoa lembrar do que digitou três segundos atrás: ela tem de VER, no
|
|
124
|
+
momento em que digita, que aquele Enter vai ao shell. Daí o âmbar — a
|
|
125
|
+
mesma cor que o resto do programa usa para "atenção, não é o caminho
|
|
126
|
+
comum". */
|
|
127
|
+
const bash = /^\s*!/.test(String(e.texto));
|
|
128
|
+
|
|
118
129
|
const out = [];
|
|
119
|
-
const regua = c.dim(BORDA.h.repeat(largura));
|
|
130
|
+
const regua = (bash ? c.warn : c.dim)(BORDA.h.repeat(largura));
|
|
120
131
|
out.push(regua);
|
|
121
132
|
|
|
122
133
|
const vazio = !String(e.texto).length;
|
|
123
134
|
linhas.forEach((l, i) => {
|
|
124
135
|
const marca = i === 0
|
|
125
|
-
? (e.ocupado ? c.dim('❯ ') : c.brand('❯ '))
|
|
136
|
+
? (e.ocupado ? c.dim('❯ ') : bash ? c.warn('$ ') : c.brand('❯ '))
|
|
126
137
|
: c.dim('┆ ');
|
|
127
|
-
|
|
138
|
+
// O "!" some do texto desenhado: quem vê "$ " já sabe que é shell, e
|
|
139
|
+
// mostrar os dois é mostrar a mesma informação duas vezes.
|
|
140
|
+
let conteudo = c.white(i === 0 && bash ? l.texto.replace(/^\s*!/, '') : l.texto);
|
|
128
141
|
// O fantasma só existe na última linha e só com o cursor no fim dela:
|
|
129
142
|
// desenhado no meio do texto, ele empurraria o que vem depois.
|
|
130
143
|
if (i === linhas.length - 1 && e.fantasma && cursor === String(e.texto).length) {
|
|
@@ -162,12 +175,22 @@ function montar(e) {
|
|
|
162
175
|
}
|
|
163
176
|
}
|
|
164
177
|
|
|
178
|
+
/* A linha que explica, só no modo bash. Ela ocupa o lugar do rodapé
|
|
179
|
+
normal de propósito: no instante em que a pessoa vai rodar um comando,
|
|
180
|
+
o que importa não é lembrar os atalhos, é saber onde ele vai rodar. */
|
|
181
|
+
if (bash) {
|
|
182
|
+
out.push(' ' + c.warn('roda no shell') + c.dim(`, em ${e.pasta || 'a pasta atual'}`)
|
|
183
|
+
+ c.dim(' · apague o ! para voltar ao normal'));
|
|
184
|
+
}
|
|
165
185
|
for (const linha of (e.status || [])) out.push(' ' + linha);
|
|
166
186
|
|
|
167
187
|
return {
|
|
168
188
|
linhas: out,
|
|
169
189
|
cursorLinha: 1 + cursorLinha, // +1 pela régua de cima
|
|
170
|
-
|
|
190
|
+
/* No modo bash o "!" não é desenhado, então tudo depois dele anda uma
|
|
191
|
+
coluna para a esquerda — e o cursor tem de andar junto, ou ele
|
|
192
|
+
aparece um caractere à frente de onde o texto realmente está. */
|
|
193
|
+
cursorColuna: 1 + 2 + cursorColuna - (bash && cursorLinha === 0 ? 1 : 0),
|
|
171
194
|
};
|
|
172
195
|
}
|
|
173
196
|
|
package/lib/skills.js
CHANGED
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
const fs = require('fs');
|
|
48
48
|
const os = require('os');
|
|
49
49
|
const path = require('path');
|
|
50
|
+
const crypto = require('crypto');
|
|
50
51
|
|
|
51
52
|
const ORIGEM = path.join(__dirname, '..', 'skills');
|
|
52
53
|
const CASA = path.join(os.homedir(), '.primocode', 'skills');
|
|
@@ -90,24 +91,98 @@ function salvarEstado(estado) {
|
|
|
90
91
|
} catch { return false; }
|
|
91
92
|
}
|
|
92
93
|
|
|
93
|
-
/*
|
|
94
|
-
|
|
95
|
-
|
|
94
|
+
/* A impressão digital do conteúdo. Serve para responder UMA pergunta: o
|
|
95
|
+
arquivo que está na casa da pessoa é o que o produto instalou, ou ela
|
|
96
|
+
mexeu nele? */
|
|
97
|
+
function impressao(texto) {
|
|
98
|
+
return crypto.createHash('sha256').update(String(texto || ''), 'utf8').digest('hex').slice(0, 16);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Traz as skills do produto para a casa da pessoa — e ATUALIZA as que ela não
|
|
103
|
+
* tocou.
|
|
104
|
+
*
|
|
105
|
+
* ── O DEFEITO QUE ISTO CONSERTA ─────────────────────────────────────────
|
|
106
|
+
* Era `if (fs.existsSync(destino)) continue;`. Uma vez instalada, a skill do
|
|
107
|
+
* produto NUNCA mais era atualizada. Quem rodou o PrimoCode um dia ficou com
|
|
108
|
+
* a versão daquele dia para sempre, e toda melhoria que veio depois — texto
|
|
109
|
+
* melhor, gatilho novo, regra corrigida — nunca chegou a ninguém. Do lado de
|
|
110
|
+
* quem usa, o sintoma é "as skills não estão funcionando": elas rodam, só que
|
|
111
|
+
* são as de antes.
|
|
112
|
+
*
|
|
113
|
+
* ── E POR QUE NÃO SOBRESCREVER SEMPRE ───────────────────────────────────
|
|
114
|
+
* Porque a pessoa pode ter editado. Sobrescrever apagaria o trabalho dela sem
|
|
115
|
+
* avisar, que é pior que a skill desatualizada. Então guarda-se, ao instalar,
|
|
116
|
+
* a impressão digital do que foi instalado: se o arquivo ainda bate com ela,
|
|
117
|
+
* ninguém mexeu e dá para atualizar em silêncio. Se não bate, a edição é
|
|
118
|
+
* dela e fica como está.
|
|
119
|
+
*
|
|
120
|
+
* A skill CRIADA pela pessoa não tem impressão nenhuma, e por isso nunca é
|
|
121
|
+
* tocada — não veio do produto.
|
|
122
|
+
*/
|
|
96
123
|
function prepararCasa() {
|
|
97
124
|
fs.mkdirSync(CASA, { recursive: true });
|
|
98
125
|
let nascidas = 0;
|
|
126
|
+
let atualizadas = 0;
|
|
127
|
+
const preservadas = [];
|
|
128
|
+
const guardadas = [];
|
|
99
129
|
let origens = [];
|
|
100
130
|
try { origens = fs.readdirSync(ORIGEM, { withFileTypes: true }); } catch { return 0; }
|
|
131
|
+
|
|
101
132
|
for (const item of origens) {
|
|
102
133
|
if (!item.isDirectory()) continue;
|
|
103
134
|
const destino = path.join(CASA, item.name);
|
|
104
|
-
|
|
135
|
+
const arquivoDestino = path.join(destino, 'SKILL.md');
|
|
136
|
+
const marca = path.join(destino, '.origem.json');
|
|
137
|
+
let novo;
|
|
138
|
+
try { novo = fs.readFileSync(path.join(ORIGEM, item.name, 'SKILL.md'), 'utf8'); }
|
|
139
|
+
catch { continue; }
|
|
140
|
+
|
|
105
141
|
try {
|
|
106
|
-
fs.
|
|
107
|
-
|
|
108
|
-
|
|
142
|
+
if (!fs.existsSync(arquivoDestino)) {
|
|
143
|
+
fs.mkdirSync(destino, { recursive: true });
|
|
144
|
+
fs.writeFileSync(arquivoDestino, novo, 'utf8');
|
|
145
|
+
fs.writeFileSync(marca, JSON.stringify({ sha: impressao(novo) }), 'utf8');
|
|
146
|
+
nascidas++;
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const atual = fs.readFileSync(arquivoDestino, 'utf8');
|
|
151
|
+
if (impressao(atual) === impressao(novo)) continue; // já é a nova
|
|
152
|
+
|
|
153
|
+
let instalada = null;
|
|
154
|
+
try { instalada = JSON.parse(fs.readFileSync(marca, 'utf8')).sha; } catch { /* sem marca */ }
|
|
155
|
+
|
|
156
|
+
if (instalada && instalada === impressao(atual)) {
|
|
157
|
+
// Intacta desde a instalação: pode atualizar sem perder nada.
|
|
158
|
+
fs.writeFileSync(arquivoDestino, novo, 'utf8');
|
|
159
|
+
fs.writeFileSync(marca, JSON.stringify({ sha: impressao(novo) }), 'utf8');
|
|
160
|
+
atualizadas++;
|
|
161
|
+
} else if (instalada) {
|
|
162
|
+
// Tem marca e não bate: a pessoa editou. Fica como está — o
|
|
163
|
+
// trabalho dela vale mais que a atualização.
|
|
164
|
+
preservadas.push(item.name);
|
|
165
|
+
} else {
|
|
166
|
+
/* SEM MARCA: instalada antes desta correção existir.
|
|
167
|
+
Aqui não dá para saber se a pessoa editou ou se é só uma
|
|
168
|
+
versão antiga do produto — e as duas saídas puras são
|
|
169
|
+
ruins: preservar congela a skill para sempre (que é o
|
|
170
|
+
defeito que estamos consertando, e atingiria justamente
|
|
171
|
+
quem já usa), sobrescrever pode apagar o trabalho dela.
|
|
172
|
+
|
|
173
|
+
Então atualiza E GUARDA O ANTERIOR ao lado. Ninguém perde
|
|
174
|
+
nada, todo mundo recebe a versão nova, e quem tinha algo
|
|
175
|
+
próprio encontra o arquivo ali para trazer de volta. Vale
|
|
176
|
+
uma vez só: da próxima já existe marca. */
|
|
177
|
+
try { fs.writeFileSync(arquivoDestino + '.anterior', atual, 'utf8'); } catch {}
|
|
178
|
+
fs.writeFileSync(arquivoDestino, novo, 'utf8');
|
|
179
|
+
fs.writeFileSync(marca, JSON.stringify({ sha: impressao(novo) }), 'utf8');
|
|
180
|
+
atualizadas++;
|
|
181
|
+
guardadas.push(item.name);
|
|
182
|
+
}
|
|
109
183
|
} catch { /* uma que falha não impede as outras */ }
|
|
110
184
|
}
|
|
185
|
+
prepararCasa.ultimo = { nascidas, atualizadas, preservadas, guardadas };
|
|
111
186
|
return nascidas;
|
|
112
187
|
}
|
|
113
188
|
|
|
@@ -323,6 +398,7 @@ function montar(texto, op = {}) {
|
|
|
323
398
|
}
|
|
324
399
|
|
|
325
400
|
module.exports = {
|
|
401
|
+
impressao,
|
|
326
402
|
CASA, ORIGEM, MAX_CARACTERES,
|
|
327
403
|
separar, listar, ler, criar, ligar, apagar, instalarDe, apelidar,
|
|
328
404
|
paraOPedido, montar, pontuar, prepararCasa,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "primocode",
|
|
3
|
-
"version": "9.
|
|
3
|
+
"version": "9.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": {
|
|
@@ -1,28 +1,75 @@
|
|
|
1
1
|
---
|
|
2
2
|
nome: Página que converte
|
|
3
|
-
descricao: estrutura e acabamento de landing page, site e página de venda
|
|
4
|
-
quando: landing, landing page, site, página, pagina, home, hero, seção, secao, cta, conversão, conversao, vendas, institucional
|
|
3
|
+
descricao: estrutura, direção de arte e acabamento de landing page, site e página de venda
|
|
4
|
+
quando: landing, landing page, site, página, pagina, home, hero, seção, secao, cta, conversão, conversao, vendas, institucional, portfólio, portfolio, one page, onepage
|
|
5
5
|
autor: PrimoCode
|
|
6
6
|
---
|
|
7
|
-
|
|
7
|
+
Evitar erro não produz beleza. A página genérica não nasce de um erro: nasce
|
|
8
|
+
da AUSÊNCIA de decisão — quem não escolhe tipografia usa a do sistema, quem não
|
|
9
|
+
escolhe paleta usa azul, quem não escolhe ritmo centraliza tudo. Então decida,
|
|
10
|
+
por escrito, ANTES de abrir o editor:
|
|
8
11
|
|
|
9
|
-
**A
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
**1. A DIREÇÃO, em uma frase.** "Editorial e severa, como um jornal econômico."
|
|
13
|
+
"Técnica e escura, como uma ferramenta de desenvolvedor." "Calorosa e
|
|
14
|
+
artesanal." Ela decide tudo o que vem depois, e assuntos diferentes não podem
|
|
15
|
+
receber a mesma.
|
|
13
16
|
|
|
14
|
-
**
|
|
15
|
-
|
|
17
|
+
**2. A TIPOGRAFIA — duas famílias, e elas têm de CONTRASTAR.** Duas grotescas
|
|
18
|
+
parecidas é o que dá cara de modelo pronto. Combinações que funcionam:
|
|
16
19
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
| Direção | Título | Texto |
|
|
21
|
+
|---|---|---|
|
|
22
|
+
| Editorial, autoridade | Playfair Display, Fraunces | Inter, Source Serif |
|
|
23
|
+
| Técnica, produto | Space Grotesk, Sora | Inter, IBM Plex Sans |
|
|
24
|
+
| Impacto, anúncio | Anton, Archivo Black | Inter, DM Sans |
|
|
25
|
+
| Calorosa, humana | Instrument Serif, Lora | Karla, Figtree |
|
|
21
26
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
a 1440px sem rolagem horizontal.
|
|
27
|
+
Título grande é grande DE VERDADE: `clamp(2.5rem, 6vw, 5.5rem)`, peso 700+,
|
|
28
|
+
`letter-spacing: -0.03em` (fonte grande precisa apertar; fonte pequena, não).
|
|
29
|
+
Texto em 17–19px, `line-height: 1.6`, largura de 60 a 75 caracteres.
|
|
26
30
|
|
|
27
|
-
|
|
31
|
+
**3. A PALETA — do ASSUNTO, nunca a padrão.** Uma cor de marca, uma neutra
|
|
32
|
+
escura para o texto, uma clara para o fundo, e UMA de destaque usada com
|
|
33
|
+
avareza — no CTA e em mais nada. Se a empresa existe, as cores são dela: leia
|
|
34
|
+
do site com `estudio_site`. Inventar a cor de uma marca real é erro que chega
|
|
35
|
+
pronto ao cliente.
|
|
36
|
+
|
|
37
|
+
**4. O RITMO VERTICAL.** Seção não tem todas a mesma altura nem o mesmo
|
|
38
|
+
tratamento. Alterne: fundo claro → fundo escuro → claro → imagem sangrando de
|
|
39
|
+
borda a borda. Respiro grande entre seções (96–160px), pequeno dentro delas.
|
|
40
|
+
|
|
41
|
+
**5. QUEBRE A SIMETRIA.** Tudo centralizado do topo ao rodapé é o sinal número
|
|
42
|
+
um de página feita às pressas. Herói em duas colunas 7/5, número grande
|
|
43
|
+
alinhado à esquerda com o texto encostado nele, imagem que ultrapassa a
|
|
44
|
+
margem. Grade de 12 colunas, e use-a de verdade.
|
|
45
|
+
|
|
46
|
+
**6. MOVIMENTO, com parcimônia.** Entrada por `IntersectionObserver`:
|
|
47
|
+
`opacity 0 → 1` e `translateY(16px → 0)`, 400ms, `cubic-bezier(.16,1,.3,1)`,
|
|
48
|
+
escalonado em 60ms entre irmãos. Estado de hover em tudo que é clicável.
|
|
49
|
+
Sempre dentro de `@media (prefers-reduced-motion: reduce)`.
|
|
50
|
+
|
|
51
|
+
**7. OS DETALHES QUE SEPARAM PROFISSIONAL DE AMADOR.**
|
|
52
|
+
Um raio de borda só, repetido. Sombra em camadas
|
|
53
|
+
(`0 1px 2px rgba(0,0,0,.06), 0 8px 24px rgba(0,0,0,.08)`), nunca a sombra
|
|
54
|
+
padrão em tudo. `font-variant-numeric: tabular-nums` em número. `text-wrap:
|
|
55
|
+
balance` em título. Ícone de biblioteca de verdade (Lucide, Phosphor), nunca
|
|
56
|
+
emoji. `:focus-visible` visível. Imagem com `width`/`height` declarados para
|
|
57
|
+
a página não pular ao carregar.
|
|
58
|
+
|
|
59
|
+
**8. ESTRUTURA que converte**: promessa em até 8 palavras → para quem é →
|
|
60
|
+
prova (número real, nome de cliente) → o que a pessoa ganha, em 3 blocos →
|
|
61
|
+
objeção respondida → UM pedido de ação, repetido no fim. Um CTA por página:
|
|
62
|
+
dois competem entre si e a pessoa não faz nenhum.
|
|
63
|
+
|
|
64
|
+
**O QUE DENUNCIA PÁGINA GENÉRICA** — nenhum destes:
|
|
65
|
+
gradiente roxo-para-azul, tudo centralizado, sombra igual em todo cartão,
|
|
66
|
+
emoji no lugar de ícone, "Bem-vindo ao nosso site", "Soluções inovadoras",
|
|
67
|
+
três cartões idênticos lado a lado, texto de exemplo em qualquer lugar.
|
|
68
|
+
|
|
69
|
+
**ANTES DE ENTREGAR**: abra no navegador e olhe. Confira em 360px, 768px e
|
|
70
|
+
1440px, sem rolagem horizontal. Contraste de 4.5:1 no texto. Se a página
|
|
71
|
+
servisse para qualquer outra empresa trocando o logotipo, ela está genérica —
|
|
72
|
+
volte ao passo 1.
|
|
73
|
+
|
|
74
|
+
Escreva o texto de verdade, sobre o assunto de verdade. Página bonita com
|
|
28
75
|
texto vazio não convence ninguém.
|
package/voz/primo_voz/orbe.py
CHANGED
|
@@ -1,60 +1,90 @@
|
|
|
1
|
-
"""orbe.py — a
|
|
2
|
-
|
|
3
|
-
"
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
O
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
ele é usado. Onde não existe, a camada escura entra, e a orb continua sendo o
|
|
35
|
-
que importa. O que não se faz é fingir: nada aqui chama de "desfoque" o que é
|
|
36
|
-
só uma sombra.
|
|
1
|
+
"""orbe.py — a única coisa que aparece na tela no modo voz.
|
|
2
|
+
|
|
3
|
+
"ia ser a flutuante ali na parte inferior da tela, bem centralizada,
|
|
4
|
+
toda cheia de partículas, toda bonita."
|
|
5
|
+
|
|
6
|
+
── POR QUE A PRIMEIRA VERSÃO FICOU FEIA, E A CULPA É DE QUATRO COISAS ──────
|
|
7
|
+
|
|
8
|
+
1. `-transparentcolor` SÓ EXISTE NO WINDOWS. No macOS ele levanta TclError,
|
|
9
|
+
que o código engolia — e a janela ficava com o fundo opaco pintado nela.
|
|
10
|
+
Era o "quadrado feio": não era desenho, era a janela inteira aparecendo.
|
|
11
|
+
No macOS a transparência de verdade se pede de outro jeito:
|
|
12
|
+
`-transparent` mais o fundo mágico `systemTransparent`.
|
|
13
|
+
|
|
14
|
+
2. O VÉU cobria a tela inteira com 72% de preto. A ideia era destacar a orb;
|
|
15
|
+
o efeito é escurecer o trabalho da pessoa enquanto ela fala. Saiu.
|
|
16
|
+
|
|
17
|
+
3. Ela nascia no CENTRO da tela — bem no meio do que a pessoa está fazendo.
|
|
18
|
+
Agora mora embaixo, centralizada na horizontal, como uma barra flutuante.
|
|
19
|
+
|
|
20
|
+
4. Não havia partícula nenhuma. Havia três círculos concêntricos.
|
|
21
|
+
|
|
22
|
+
── O QUE ELA DESENHA ───────────────────────────────────────────────────────
|
|
23
|
+
Um núcleo com halo suave e um enxame de partículas em órbita. A cor diz o
|
|
24
|
+
estado; o MOVIMENTO diz a intensidade — as partículas se afastam e aceleram
|
|
25
|
+
com a voz, e respiram devagar quando ele está pensando. Nenhuma palavra: quem
|
|
26
|
+
está falando não lê.
|
|
27
|
+
|
|
28
|
+
── O QUE NÃO DÁ PARA FAZER EM TKINTER, E COMO SE CONTORNA ─────────────────
|
|
29
|
+
Não há desfoque nem gradiente: o Canvas do Tk desenha forma chapada. O halo é
|
|
30
|
+
feito de círculos concêntricos com a cor interpolada contra o fundo — o
|
|
31
|
+
mesmo truque de sempre, e o motivo de o fundo precisar ser conhecido. Onde a
|
|
32
|
+
janela é transparente de verdade, o "fundo" para a mistura é o preto, que é o
|
|
33
|
+
que mais se aproxima de qualquer tela.
|
|
37
34
|
"""
|
|
38
35
|
from __future__ import annotations
|
|
39
36
|
|
|
40
37
|
import math
|
|
41
38
|
import queue
|
|
39
|
+
import random
|
|
42
40
|
import sys
|
|
43
|
-
import threading
|
|
44
41
|
import tkinter as tk
|
|
45
42
|
from typing import Callable, Optional
|
|
46
43
|
|
|
47
44
|
# Os quatro estados, e a cor de cada um. A paleta é a do PrimoCode (ciano de
|
|
48
45
|
# marca, âmbar de aviso) para o modo voz não parecer outro produto.
|
|
49
46
|
CORES = {
|
|
50
|
-
"esperando": ("#0891b2", "#
|
|
47
|
+
"esperando": ("#0891b2", "#0e3a4a"), # ciano recuado: pronto, sem pressa
|
|
51
48
|
"ouvindo": ("#22d3ee", "#0e7490"), # ciano vivo: a vez é sua
|
|
52
49
|
"pensando": ("#fbbf24", "#78350f"), # âmbar: ele está trabalhando
|
|
53
50
|
"falando": ("#67e8f9", "#0891b2"), # ciano claro: a voz é dele
|
|
54
51
|
}
|
|
55
52
|
|
|
56
|
-
|
|
57
|
-
|
|
53
|
+
LARGURA = 340 # a janela, deitada: é uma barra flutuante, não um disco
|
|
54
|
+
ALTURA = 170
|
|
55
|
+
RAIO_BASE = 26 # o núcleo
|
|
56
|
+
MARGEM_INFERIOR = 72 # distância do pé da tela
|
|
57
|
+
PARTICULAS = 34
|
|
58
|
+
FUNDO_FALSO = "#05070a" # só onde o sistema não dá transparência
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _mistura(a: str, b: str, quanto: float) -> str:
|
|
62
|
+
"""Duas cores hex, uma fração — a cor no meio do caminho."""
|
|
63
|
+
quanto = max(0.0, min(1.0, quanto))
|
|
64
|
+
ra, ga, ba = int(a[1:3], 16), int(a[3:5], 16), int(a[5:7], 16)
|
|
65
|
+
rb, gb, bb = int(b[1:3], 16), int(b[3:5], 16), int(b[5:7], 16)
|
|
66
|
+
return "#%02x%02x%02x" % (
|
|
67
|
+
int(ra + (rb - ra) * quanto),
|
|
68
|
+
int(ga + (gb - ga) * quanto),
|
|
69
|
+
int(ba + (bb - ba) * quanto),
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class Particula:
|
|
74
|
+
"""Um ponto em órbita.
|
|
75
|
+
|
|
76
|
+
Cada uma tem ângulo, raio e velocidade PRÓPRIOS, sorteados uma vez. É o
|
|
77
|
+
que separa um enxame de um relógio: com os mesmos valores para todas,
|
|
78
|
+
elas giram em formação e o olho lê como engrenagem, não como vida.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
def __init__(self, rnd: random.Random) -> None:
|
|
82
|
+
self.ang = rnd.uniform(0, math.tau)
|
|
83
|
+
self.raio = rnd.uniform(1.0, 2.2) # múltiplo do raio do núcleo
|
|
84
|
+
self.giro = rnd.uniform(-0.9, 0.9) or 0.4
|
|
85
|
+
self.tam = rnd.uniform(1.2, 3.0)
|
|
86
|
+
self.fase = rnd.uniform(0, math.tau)
|
|
87
|
+
self.brilho = rnd.uniform(0.35, 1.0)
|
|
58
88
|
|
|
59
89
|
|
|
60
90
|
class Orbe:
|
|
@@ -70,61 +100,76 @@ class Orbe:
|
|
|
70
100
|
self._fila = queue.Queue()
|
|
71
101
|
self._ao_fechar = ao_fechar
|
|
72
102
|
self._vivo = True
|
|
103
|
+
self._rnd = random.Random(7) # semente fixa: o enxame é sempre o mesmo
|
|
104
|
+
self._particulas = [Particula(self._rnd) for _ in range(PARTICULAS)]
|
|
73
105
|
|
|
74
|
-
self.
|
|
75
|
-
self._montar_orbe()
|
|
106
|
+
self._montar()
|
|
76
107
|
self.raiz.protocol("WM_DELETE_WINDOW", self.fechar)
|
|
77
108
|
self.raiz.bind("<Escape>", lambda e: self.fechar())
|
|
78
109
|
self._pintar()
|
|
79
110
|
|
|
80
|
-
# ──
|
|
81
|
-
def
|
|
82
|
-
"""
|
|
111
|
+
# ── a janela ─────────────────────────────────────────────────────────
|
|
112
|
+
def _montar(self) -> None:
|
|
113
|
+
"""Sem moldura, embaixo, centralizada — e transparente onde dá.
|
|
83
114
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
115
|
+
A transparência se pede de um jeito em cada sistema, e pedir errado
|
|
116
|
+
não avisa: devolve TclError, que engolido deixa a janela OPACA. Era
|
|
117
|
+
exatamente o "quadrado feio". Por isso aqui cada caminho é explícito,
|
|
118
|
+
e o fundo falso só entra quando nenhum funcionou.
|
|
88
119
|
"""
|
|
89
|
-
self.veu = tk.Toplevel(self.raiz)
|
|
90
|
-
self.veu.overrideredirect(True)
|
|
91
|
-
largura = self.veu.winfo_screenwidth()
|
|
92
|
-
altura = self.veu.winfo_screenheight()
|
|
93
|
-
self.veu.geometry(f"{largura}x{altura}+0+0")
|
|
94
|
-
self.veu.configure(bg="#05070a")
|
|
95
|
-
try:
|
|
96
|
-
self.veu.attributes("-alpha", 0.72)
|
|
97
|
-
self.veu.attributes("-topmost", True)
|
|
98
|
-
except tk.TclError:
|
|
99
|
-
pass
|
|
100
|
-
# O véu não recebe clique: a pessoa continua usando o computador por
|
|
101
|
-
# baixo dele. Onde o sistema não deixa, ele simplesmente cobre.
|
|
102
|
-
try:
|
|
103
|
-
self.veu.attributes("-transparent", True)
|
|
104
|
-
except tk.TclError:
|
|
105
|
-
pass
|
|
106
|
-
|
|
107
|
-
# ── a orb ────────────────────────────────────────────────────────────
|
|
108
|
-
def _montar_orbe(self) -> None:
|
|
109
120
|
self.raiz.overrideredirect(True)
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
x = (
|
|
113
|
-
y =
|
|
114
|
-
self.raiz.geometry(f"{
|
|
115
|
-
|
|
121
|
+
larguraTela = self.raiz.winfo_screenwidth()
|
|
122
|
+
alturaTela = self.raiz.winfo_screenheight()
|
|
123
|
+
x = (larguraTela - LARGURA) // 2
|
|
124
|
+
y = alturaTela - ALTURA - MARGEM_INFERIOR
|
|
125
|
+
self.raiz.geometry(f"{LARGURA}x{ALTURA}+{x}+{max(0, y)}")
|
|
126
|
+
|
|
127
|
+
self.fundo = FUNDO_FALSO
|
|
128
|
+
transparente = False
|
|
129
|
+
|
|
130
|
+
if sys.platform == "darwin":
|
|
131
|
+
# macOS: `-transparent` mais o fundo mágico. Os dois juntos, ou
|
|
132
|
+
# nenhum dos dois funciona.
|
|
133
|
+
try:
|
|
134
|
+
self.raiz.attributes("-transparent", True)
|
|
135
|
+
self.fundo = "systemTransparent"
|
|
136
|
+
transparente = True
|
|
137
|
+
except tk.TclError:
|
|
138
|
+
pass
|
|
139
|
+
elif sys.platform == "win32":
|
|
140
|
+
try:
|
|
141
|
+
self.raiz.attributes("-transparentcolor", FUNDO_FALSO)
|
|
142
|
+
transparente = True
|
|
143
|
+
except tk.TclError:
|
|
144
|
+
pass
|
|
145
|
+
|
|
116
146
|
try:
|
|
117
147
|
self.raiz.attributes("-topmost", True)
|
|
118
|
-
# A cor de fundo vira transparente onde o sistema deixa (Windows),
|
|
119
|
-
# e no resto ela é a mesma do véu — o que dá o mesmo efeito.
|
|
120
|
-
self.raiz.attributes("-transparentcolor", "#05070a")
|
|
121
148
|
except tk.TclError:
|
|
122
149
|
pass
|
|
123
150
|
|
|
124
|
-
self.
|
|
125
|
-
|
|
151
|
+
self.raiz.configure(bg=self.fundo)
|
|
152
|
+
self.tela = tk.Canvas(self.raiz, width=LARGURA, height=ALTURA,
|
|
153
|
+
bg=self.fundo, highlightthickness=0, bd=0)
|
|
126
154
|
self.tela.pack()
|
|
127
155
|
|
|
156
|
+
# Para a MISTURA de cor do halo, o fundo tem de ser um valor de cor —
|
|
157
|
+
# e "systemTransparent" não é. Contra o preto é a aproximação que
|
|
158
|
+
# some melhor em qualquer tela.
|
|
159
|
+
self.fundoDaMistura = FUNDO_FALSO if transparente else self.fundo
|
|
160
|
+
|
|
161
|
+
# Sem moldura não há barra de título para arrastar: o arrasto é na
|
|
162
|
+
# janela toda. Quem quiser a orb noutro canto, leva.
|
|
163
|
+
self.raiz.bind("<Button-1>", self._pegar)
|
|
164
|
+
self.raiz.bind("<B1-Motion>", self._arrastar)
|
|
165
|
+
self._pego = (0, 0)
|
|
166
|
+
|
|
167
|
+
def _pegar(self, e) -> None:
|
|
168
|
+
self._pego = (e.x_root - self.raiz.winfo_x(), e.y_root - self.raiz.winfo_y())
|
|
169
|
+
|
|
170
|
+
def _arrastar(self, e) -> None:
|
|
171
|
+
self.raiz.geometry(f"+{e.x_root - self._pego[0]}+{e.y_root - self._pego[1]}")
|
|
172
|
+
|
|
128
173
|
# ── o desenho ────────────────────────────────────────────────────────
|
|
129
174
|
def _pintar(self) -> None:
|
|
130
175
|
if not self._vivo:
|
|
@@ -132,124 +177,112 @@ class Orbe:
|
|
|
132
177
|
while True:
|
|
133
178
|
try:
|
|
134
179
|
estado, nivel = self._fila.get_nowait()
|
|
135
|
-
if estado ==
|
|
136
|
-
self.
|
|
180
|
+
if estado == "__esconder__":
|
|
181
|
+
self._aplicarVisibilidade(False)
|
|
137
182
|
continue
|
|
138
|
-
if estado ==
|
|
139
|
-
self.
|
|
183
|
+
if estado == "__mostrar__":
|
|
184
|
+
self._aplicarVisibilidade(True)
|
|
140
185
|
continue
|
|
186
|
+
if estado:
|
|
187
|
+
self.estado = estado
|
|
188
|
+
if nivel is not None:
|
|
189
|
+
self.nivel = max(0.0, min(1.0, nivel))
|
|
141
190
|
except queue.Empty:
|
|
142
191
|
break
|
|
143
|
-
if estado:
|
|
144
|
-
self.estado = estado
|
|
145
|
-
if nivel is not None:
|
|
146
|
-
self.nivel = max(0.0, min(1.0, nivel))
|
|
147
192
|
|
|
148
|
-
self.
|
|
193
|
+
self._fase += 0.045
|
|
149
194
|
vivo, fundo = CORES.get(self.estado, CORES["esperando"])
|
|
150
|
-
|
|
151
|
-
self.
|
|
195
|
+
cx, cy = LARGURA / 2, ALTURA / 2
|
|
196
|
+
self.tela.delete("all")
|
|
152
197
|
|
|
153
|
-
#
|
|
154
|
-
#
|
|
198
|
+
# A respiração de cada estado. Quem espera respira devagar; quem
|
|
199
|
+
# pensa, num ritmo próprio; quem ouve pulsa com a VOZ, não com o
|
|
200
|
+
# relógio — é o que faz a orb parecer estar escutando de verdade.
|
|
155
201
|
if self.estado == "ouvindo":
|
|
156
|
-
|
|
157
|
-
raio = RAIO_BASE * (1 + 0.35 * self.nivel)
|
|
158
|
-
pulso = 1.0
|
|
202
|
+
pulso = 0.25 + self.nivel * 1.15
|
|
159
203
|
elif self.estado == "pensando":
|
|
160
|
-
|
|
161
|
-
raio = RAIO_BASE * (1 + 0.06 * math.sin(self._fase * 0.8))
|
|
162
|
-
pulso = 0.8 + 0.2 * math.sin(self._fase * 0.8)
|
|
204
|
+
pulso = 0.42 + 0.30 * math.sin(self._fase * 2.1)
|
|
163
205
|
elif self.estado == "falando":
|
|
164
|
-
|
|
165
|
-
pulso = 1.0
|
|
206
|
+
pulso = 0.55 + 0.38 * math.sin(self._fase * 3.4)
|
|
166
207
|
else:
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
#
|
|
172
|
-
#
|
|
173
|
-
|
|
174
|
-
for i in range(
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
self.
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
208
|
+
pulso = 0.16 + 0.10 * math.sin(self._fase * 0.9)
|
|
209
|
+
|
|
210
|
+
raio = RAIO_BASE * (1 + pulso * 0.42)
|
|
211
|
+
|
|
212
|
+
# HALO: círculos concêntricos, do mais largo e apagado ao núcleo. É o
|
|
213
|
+
# desfoque que o Canvas não tem — e por isso a cor de cada anel é
|
|
214
|
+
# interpolada contra o fundo, em vez de ter opacidade.
|
|
215
|
+
for i in range(11, 0, -1):
|
|
216
|
+
f = i / 11
|
|
217
|
+
r = raio * (1 + f * 2.6)
|
|
218
|
+
cor = _mistura(self.fundoDaMistura, fundo, (1 - f) ** 2 * 0.85)
|
|
219
|
+
self.tela.create_oval(cx - r, cy - r, cx + r, cy + r, fill=cor, outline="")
|
|
220
|
+
|
|
221
|
+
# PARTÍCULAS: entre o halo e o núcleo, para o núcleo ficar por cima.
|
|
222
|
+
for p in self._particulas:
|
|
223
|
+
p.ang += p.giro * 0.012 * (0.6 + pulso)
|
|
224
|
+
# O raio respira junto com a voz: quanto mais alto se fala, mais
|
|
225
|
+
# longe o enxame se abre.
|
|
226
|
+
osc = math.sin(self._fase * 1.7 + p.fase)
|
|
227
|
+
r = raio * (p.raio + 0.30 * osc + pulso * 0.55)
|
|
228
|
+
px = cx + math.cos(p.ang) * r * 1.55 # elipse: a janela é deitada
|
|
229
|
+
py = cy + math.sin(p.ang) * r * 0.80
|
|
230
|
+
t = p.tam * (0.7 + pulso * 0.8)
|
|
231
|
+
cor = _mistura(self.fundoDaMistura, vivo, p.brilho * (0.35 + pulso * 0.65))
|
|
232
|
+
self.tela.create_oval(px - t, py - t, px + t, py + t, fill=cor, outline="")
|
|
233
|
+
|
|
234
|
+
# NÚCLEO: dois discos, o de dentro mais claro. Dá volume sem gradiente.
|
|
235
|
+
self.tela.create_oval(cx - raio, cy - raio, cx + raio, cy + raio,
|
|
236
|
+
fill=_mistura(fundo, vivo, 0.72), outline="")
|
|
237
|
+
rn = raio * 0.55
|
|
238
|
+
self.tela.create_oval(cx - rn, cy - rn, cx + rn, cy + rn,
|
|
239
|
+
fill=_mistura(vivo, "#ffffff", 0.35), outline="")
|
|
187
240
|
|
|
188
241
|
self.raiz.after(33, self._pintar)
|
|
189
242
|
|
|
190
|
-
|
|
191
|
-
def _misturar(a: str, b: str, quanto: float) -> str:
|
|
192
|
-
"""`a` misturado com `b`. tkinter não tem alpha por forma, então a
|
|
193
|
-
transparência é resolvida na cor."""
|
|
194
|
-
quanto = max(0.0, min(1.0, quanto))
|
|
195
|
-
ra, ga, ba = int(a[1:3], 16), int(a[3:5], 16), int(a[5:7], 16)
|
|
196
|
-
rb, gb, bb = int(b[1:3], 16), int(b[3:5], 16), int(b[5:7], 16)
|
|
197
|
-
r = round(ra + (rb - ra) * quanto)
|
|
198
|
-
g = round(ga + (gb - ga) * quanto)
|
|
199
|
-
bl = round(ba + (bb - ba) * quanto)
|
|
200
|
-
return f"#{r:02x}{g:02x}{bl:02x}"
|
|
201
|
-
|
|
202
|
-
# ── de fora ──────────────────────────────────────────────────────────
|
|
203
|
-
def mudar(self, estado: Optional[str] = None, nivel: Optional[float] = None) -> None:
|
|
204
|
-
"""Chamado de OUTRAS threads. tkinter não é seguro fora da thread
|
|
205
|
-
principal, então o que chega vai para uma fila e é lido no desenho."""
|
|
206
|
-
self._fila.put((estado, nivel))
|
|
207
|
-
|
|
243
|
+
# ── visibilidade ─────────────────────────────────────────────────────
|
|
208
244
|
def esconder(self) -> None:
|
|
209
245
|
"""Some da tela sem morrer.
|
|
210
246
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
o errado: recriar uma Tk leva centenas de milissegundos, pisca, e em
|
|
217
|
-
alguns sistemas rouba o foco da janela onde a pessoa estava digitando.
|
|
218
|
-
`withdraw` tira da tela e guarda tudo de pé.
|
|
247
|
+
No modo de espera ele fica horas sem nada acontecer, e uma orb parada
|
|
248
|
+
na tela todo esse tempo é estorvo. Destruir e recriar a janela a cada
|
|
249
|
+
despertar seria o caminho óbvio, e é o errado: recriar uma Tk leva
|
|
250
|
+
centenas de milissegundos, pisca, e em alguns sistemas rouba o foco da
|
|
251
|
+
janela onde a pessoa estava digitando. `withdraw` guarda tudo de pé.
|
|
219
252
|
"""
|
|
220
253
|
self._fila.put(("__esconder__", None))
|
|
221
254
|
|
|
222
255
|
def mostrar(self) -> None:
|
|
223
|
-
"""Volta para a tela, no centro, por cima de tudo."""
|
|
224
256
|
self._fila.put(("__mostrar__", None))
|
|
225
257
|
|
|
226
|
-
def
|
|
258
|
+
def _aplicarVisibilidade(self, visivel: bool) -> None:
|
|
227
259
|
# Só na thread da interface: tkinter não perdoa chamada de fora.
|
|
228
260
|
try:
|
|
229
261
|
if visivel:
|
|
230
|
-
self.veu.deiconify()
|
|
231
262
|
self.raiz.deiconify()
|
|
232
263
|
self.raiz.lift()
|
|
233
264
|
if sys.platform != "linux":
|
|
234
265
|
self.raiz.attributes("-topmost", True)
|
|
235
266
|
else:
|
|
236
267
|
self.raiz.withdraw()
|
|
237
|
-
self.veu.withdraw()
|
|
238
268
|
except Exception:
|
|
239
269
|
pass # janela já fechando: nada a fazer
|
|
240
270
|
self.visivel = visivel
|
|
241
271
|
|
|
272
|
+
# ── ciclo de vida ────────────────────────────────────────────────────
|
|
273
|
+
def mudar(self, estado: Optional[str] = None, nivel: Optional[float] = None) -> None:
|
|
274
|
+
self._fila.put((estado, nivel))
|
|
275
|
+
|
|
242
276
|
def fechar(self) -> None:
|
|
243
277
|
if not self._vivo:
|
|
244
278
|
return
|
|
245
279
|
self._vivo = False
|
|
246
|
-
|
|
247
|
-
|
|
280
|
+
try:
|
|
281
|
+
if self._ao_fechar:
|
|
248
282
|
self._ao_fechar()
|
|
249
|
-
|
|
250
|
-
|
|
283
|
+
except Exception:
|
|
284
|
+
pass
|
|
251
285
|
try:
|
|
252
|
-
self.raiz.quit()
|
|
253
286
|
self.raiz.destroy()
|
|
254
287
|
except Exception:
|
|
255
288
|
pass
|