DeepSeek Harness Remote
DeepSeek Harness Remote 可将手机或另一台电脑与你运行 DeepSeek Harness 的电脑安全连接,随时随地继续你的工作,方便随时查看进度并继续工作。
AI 翻译
尚未检测到更新。GitHub 发布版本后会显示在这里。
个人 GitHub 账号下公开仓库的所有者可以登录并验证所有权,自行管理介绍,无需管理员批准。
登录并生成验证文件,在 30 分钟内将其提交到此仓库的默认分支。其他分支或分叉仓库中的文件无效。然后在下方检查文件,即可无需管理员批准直接管理项目。请保留文件以维持管理权限。
如需登录,将前往登录页面。登录后请回到此处,再次点击所选的验证按钮。
在项目文件夹中运行 /nomorevibe verify,即可验证所有权并自行管理介绍。
一分钟完成验证Editor de texto completo para arquivos grandes: cursor de caractere e digitacao livre num arquivo que continua no disco. Tabela de pecas sobre mmap.
翻译准备中
取自仓库 README
正在加载完整 README。
查看完整 README ↗Editor de texto completo para arquivos grandes: cursor de caractere, digitação livre, seleção atravessando linhas — num arquivo de 1 GB que continua no disco.
O TextForge abre 240 MB
instantaneamente e, desde a v0.3.0, edita linha a linha: F2 abre um campo
sobre a linha, Enter confirma. Isso resolve corrigir um valor num export de
ERP. Não resolve escrever.
Este projeto é o outro lado da mesma moeda. Mesma economia de memória, sem o modo de edição por linha.
É a única coisa que precisa ser entendida antes de mexer em qualquer arquivo daqui:
O documento não é texto na memória. É uma lista de faixas — umas apontando para o arquivo no disco, outras para o que você digitou.
peças = [ (ORIGINAL, 0, 4.812.003 bytes, 120.301 linhas), ← nunca foi lido
(ADICIONADO, 0, 27 bytes, 0 linhas), ← o que você digitou
(ORIGINAL, 4.812.050, 8.400.112 bytes, 210.884 linhas) ]
O documento acima tem 13 MB e o que está na RAM são 27 bytes. É a estrutura clássica de editor (piece table), escolhida por três propriedades:
str.A diferença para o TextForge é a granularidade: lá a unidade é a linha, aqui é o byte — e é isso que permite o cursor andar caractere a caractere.
Já dá para abrir, digitar e salvar:
.venv\Scripts\python.exe app.py caminho\do\arquivo.txt
| Parte | Situação |
|---|---|
tfedit/original.py — mmap + índice esparso incremental | pronto |
tfedit/pecas.py — tabela de peças, desfazer, fusão de digitação | pronto |
tfedit/gravacao.py — gravação por streaming, troca atômica | pronto |
tfedit/codificacao.py — cascata de detecção, fim de linha | pronto |
tfedit/janela.py — a janela viva e a escrita de volta mínima | pronto |
tfedit/busca.py — localizar e substituir no documento inteiro | pronto |
tfedit/log_interno.py — log e captura de erro não tratado | pronto |
tfedit/interface/ — abas, editor deslizante, barra de busca | pronto |
Empacotamento (build.bat, .spec, ZIP) | pronto |
tfedit/configuracao.py — preferências, recentes, limites | pronto |
tfedit/cli.py — --linha, vários arquivos, recusa de dispositivos | pronto |
tfedit/sessao.py — sessão pelo diário de edições | pronto |
tfedit/instancia_unica.py — uma janela só, "Abrir com" | pronto |
tfedit/realce/ + tfedit/linguagens/ — realce, 24 linguagens | pronto |
tfedit/tema.py — temas claro/escuro, temas do usuário | pronto |
tfedit/conversao.py — reinterpretar e converter a codificação | pronto |
tfedit/interface/visualizadores/ — views: hexadecimal e grade CSV | pronto |
tfedit/csv_dialeto.py — detecção de separador, aspas e cabeçalho | pronto |
tfedit/planilha/ — .xlsx: leitura e gravação por patch no ZIP | pronto |
tfedit/formatadores/ — JSON, XML, CSS, HTML e SQL | pronto |
Medido, arquivo de 18 MB com 400 mil linhas: digitar duas frases (uma no
começo, outra na linha 300.000, com deslize entre elas) deixa 42 bytes na
memória. O QPlainTextEdit segura 5.001 blocos — a fatia —, e não as 400.001
linhas. Gravar preserva o CRLF das 400.000 linhas e deixa intactas as que não
foram tocadas.
Menu Visualizar (ou o campo do rodapé) troca entre Texto e Hexadecimal
sem reabrir nada: as duas são views do mesmo documento, e o mmap, o índice e
a tabela de peças são os mesmos.
00000000 48 65 6c 6c 6f 2c 20 6d 75 6e 64 6f 21 0d 0a 41 |Hello, mundo!..A|
Funciona em 1 GB porque desenha sozinho. Um QTableView com modelo virtual
quebraria em dois pontos: a barra de rolagem em pixels estoura o inteiro de 32
bits um pouco acima de 4 GB de arquivo, e o modelo seria consultado 17 vezes por
linha visível (uma por célula e por papel). Aqui a rolagem é em unidade de
linha — o mesmo inteiro aguenta 32 GB — e cada linha são três drawText.
Uma leitura por repintura. O paintEvent pede ao documento um bloco com
tudo o que a tela mostra. Medido no teste: 752 bytes para desenhar uma tela
de um arquivo de 2 MB. O custo por quadro não depende do tamanho do arquivo.
Ele lê o documento, não o disco: o que você digitou no modo texto e ainda
não gravou aparece no dump. Ctrl+G vira "ir para deslocamento" e aceita 1024
ou 0x400. Copiar tem teto de 1 MB — a área de transferência guarda o texto
inteiro na memória.
A grade do TextForge não serve aqui, e não é questão de ajuste: ela fatia o
texto inteiro em uma str por registro. Para 240 MB isso são vários GB.
Aqui o modelo não tem os dados. rowCount é o total de linhas do índice, e
cada célula é lida na hora por Documento.faixa(), em blocos de 256 linhas.
Medido no teste: abrir um CSV de 200 mil linhas lê 456 delas.
Editar uma célula troca só os bytes daquele campo. Reconstruir o registro
inteiro — o caminho do irmão — reescreveria todos os campos com o quoting
mínimo do módulo csv, tirando aspas legítimas de campos que ninguém tocou. E
o fim de linha vem da própria linha, não do perfil do arquivo: em arquivo
com CRLF e LF misturados, usar o majoritário trocaria bytes que ninguém mandou
trocar. Cada célula editada é uma operação de desfazer.
A detecção do separador é pela consistência, não pela frequência — e ganhou um critério a mais que o irmão não tem. Num export brasileiro:
produto;preco;desconto <- o cabeçalho tem ';' e nenhuma vírgula
Parafuso 3,5mm 0;0,50;0,00 <- nos dados, ambos são uniformes
Vírgula e ponto e vírgula são os dois 100% consistentes, e a vírgula tem contagem maior. O que decide é a presença: um separador de verdade está em todas as linhas, inclusive no cabeçalho.
O que a grade não faz, e diz que não faz. Um campo entre aspas pode conter quebra de linha; descobrir isso exigiria varrer do byte 0. Então aqui uma linha é um registro — e as linhas com aspas sem fechar aparecem marcadas e não são editáveis, com a explicação na dica. Não se corrompe o que não se consegue analisar. O cabeçalho também não ordena: um clique acionaria a leitura do arquivo inteiro.
Um .xlsx não é texto — é um ZIP de XML. Não tem linha, nem fim de linha,
nem codificação, e indexar 100 MB de ZIP contando produziria um número sem
sentido. Por isso uma aba de planilha não cria mmap, índice nem tabela de
peças: editor, documento e original ficam None, e a única visualização é
a grade.
O princípio que se mantém é o mesmo, um formato acima: o que ninguém tocou sai como entrou. Abrir e salvar sem editar devolve o arquivo byte a byte — o pacote original nem é recomprimido. Editar uma célula vira um patch nos bytes daquela célula, dentro do ZIP remontado na ordem original.
Isso não é preciosismo. Uma planilha regravada do zero perde formatação, gráficos e tabelas dinâmicas — e o Excel costuma abrir assim mesmo, sem avisar que o arquivo empobreceu. O teste verifica que o formato de moeda da coluna sobrevive à edição de uma célula vizinha.
O teto de tamanho é consequência, não descuido. Como a pasta inteira vai
para a memória, o tamanho é conferido por stat antes de ler um byte. Acima
de 100 MB o arquivo abre como arquivo comum, onde as garantias de memória do
editor voltam a valer. Um .zip renomeado para .xlsx também é recusado — a
detecção olha o conteúdo.
Ferramentas → Comparar arquivos (Ctrl+D): dois painéis alinhados, com F7 e
Shift+F7 pulando de diferença em diferença. Verde o que só existe à direita,
vermelho o que só existe à esquerda, âmbar o que mudou.
O difflib sozinho não serve, e isso foi medido. Dois arquivos de 200 mil
linhas com uma mudança a cada dez — duas versões de um export, 10% das linhas
alteradas — passavam de dois minutos:
| linhas | tempo | |
|---|---|---|
| 5 mil | 0,24 s | |
| 10 mil | 0,96 s | 4× |
| 20 mil | 4,63 s | 4,8× |
| 40 mil | 27,94 s | 6× |
Dobrar o tamanho quintuplicava o tempo. Não era um teto que resolvia: seria preciso recusar qualquer arquivo acima de umas 20 mil linhas.
A saída foi ancorar nas linhas únicas, como o diff de paciência do git: uma
linha que aparece exatamente uma vez nos dois arquivos só pode corresponder a si
mesma. As âncoras cortam o problema em pedaços pequenos, e o difflib só roda
dentro de cada pedaço. O mesmo caso passou a 0,37 s.
Três decisões de memória, todas medidas:
CRLF contra LF não é diferença: quem quer ver o terminador usa o hexadecimal.
Doze botões em quatro grupos — arquivo, edição, busca, visualização —, e a lista
de quais aparecem fica em Configurações → Barra de atalhos. A ordem salva é
a da barra, e marcar ou desmarcar vale na hora.
Os ícones são desenhados em código, não arquivos de imagem. Três motivos, e o terceiro decide:
QStyle.StandardPixmap, e metade da barra ficaria sem
símbolo;O teste mede cada ícone reduzido a 16 px e reprova o que ficar com menos de 20 pixels opacos — é a mesma lição que o ícone do programa ensinou: abaixo disso o símbolo não diz mais nada.
Uma chave desconhecida na configuração é ignorada em silêncio: um arquivo gravado por uma versão mais nova não pode impedir esta de abrir.
Editar → Selecionar tudo (Ctrl+A) seleciona o documento, e não só a parte
carregada. Antes, o Ctrl+A do widget só conhecia a fatia de até 5.000 linhas, e
copiar depois dele levava um pedaço do arquivo.
Com o arquivo inteiro selecionado:
Na tela, só a parte carregada aparece selecionada; o rodapé diz que é o arquivo inteiro, com o número de linhas e o tamanho. Medido num arquivo de 45 MB: copiou as 143.541 linhas.
Na barra de busca (Ctrl+F):
A varredura roda aos pedaços na própria interface, e não numa thread: assim ninguém lê a tabela de peças enquanto você edita. Medido num arquivo de 45 MB: 18.330 ocorrências listadas em 0,4 s. A grade guarda no máximo 100.000 ocorrências; acima disso para de crescer e avisa, mas a contagem continua sendo a do arquivo inteiro.
A lista é um retrato do momento da busca. Editar depois pode deslocar linhas; Listar de novo refaz. Trocar de aba fecha a lista.
Quebra de linha passou a ter uma regra só no programa inteiro: o , ou um
que não venha seguido de . Num CRLF a quebra é o ; um CR sozinho
(Mac antigo e muito export de sistema legado) também é quebra.
O defeito: um arquivo de 143.541 linhas abria como 23.926, e o fim do editor
mostrava 48.915. Ele tinha 119.615 quebras só com CR, 23.923 CRLF e 2 LF. O
índice contava apenas o ; o editor normalizava o CR e mostrava as linhas
certas. Número de linha, Ir para linha e Ctrl+End ficavam presos no total
errado. Agora o índice conta 143.541, em 0,15 s.
A regra mantém o que o modelo inteiro usa: cada quebra é identificada por um byte, e a linha seguinte começa logo depois dele. Um CRLF partido entre dois blocos da varredura conta uma vez só, porque a regra olha o byte seguinte no arquivo, e não no bloco. Na tabela de peças, a peça antes de cada edição é recontada olhando a peça vizinha. Um teste com 600 edições ao acaso, com desfazer e refazer, compara a contagem com um cálculo do zero a cada passo.
Em UTF-16 a regra fica desligada: o byte 0x0D aparece dentro de caracteres
comuns (č é 0D 01).
Medido no mesmo arquivo: inserir um caractere e salvar convertia 833 CRLF em CR. A fatia era regravada com o terminador dominante, e tudo entre o primeiro e o último ponto diferente era reescrito, sem aviso. Agora cada linha volta com o terminador que tinha; só uma linha nova recebe o dominante. Inserir um caractere muda exatamente um byte.
Em arquivo grande, a varredura do índice ainda pode estar no começo quando a caixa de Ir para linha (Ctrl+G) abre. O total era lido naquele instante, e um arquivo de 143 mil linhas mostrava "1 a 23.926". Agora a caixa espera a varredura terminar e usa o total do arquivo inteiro. Ctrl+End também.
O rodapé (Ln … de …) também acompanha o total: antes, depois de um Ir para linha ou de a varredura terminar, ele mostrava o total antigo.
O índice é leve: cerca de 0,34 MB por 10 milhões de linhas. Por isso o arquivo abre sempre inteiro; não há limite de linhas nem modo parcial.
Arquivo → Exportar para CSV... grava a aba atual da planilha num CSV novo, com
o separador e a codificação escolhidos. A planilha aberta não muda: o .xlsx
continua byte a byte igual.
; por padrão, que é o que o Excel em português lê sem
perguntar. Também ,, TAB, |, :, ~, ^, # ou espaço.; (ou o separador escolhido) dentro vão entre aspas, para
não virar outra coluna.O CSV sai com o que a grade mostra, edições ainda não gravadas inclusive.
Uma célula de fórmula sai pelo valor calculado que o Excel guardou, e não
pela fórmula. Se a codificação não escrever algum caractere — um Ę em
ISO-8859-1, por exemplo — a exportação recusa, nomeia o caractere e não grava
nada. Nenhum caractere é trocado por ? em silêncio.
Exporta uma aba por vez: a aba visível. Para outra aba, troque de aba e exporte de novo.
Duas falhas do mesmo gesto. Na grade de colunas de um CSV, Ctrl+C não copiava nada: a grade não tinha o comando, e a tecla caía no editor de texto escondido, que estava vazio. Na planilha, copiar levava só a primeira célula.
Agora as duas usam a mesma função. Ela copia o intervalo selecionado no formato que o Excel cola: colunas separadas por TAB, linhas por quebra de linha. Um valor com TAB ou quebra dentro vai entre aspas, para não virar outra célula ao colar.
Há um limite de 200 mil células por cópia, porque a área de transferência guarda o texto inteiro na memória. Acima disso a cópia é recusada e a barra diz o motivo — o mesmo caminho do hexadecimal. A recusa da planilha, que antes não era mostrada em lugar nenhum, agora aparece na barra.
Selecionar uma coluna e copiar levava só a primeira célula. O copiar lia
apenas a célula com o cursor e ignorava o resto da seleção.
Agora copia o retângulo selecionado no formato que o Excel cola: colunas separadas por TAB, linhas por quebra de linha. Numa seleção irregular, as células que não estão selecionadas saem vazias, para as colunas continuarem alinhadas. Um valor com TAB ou quebra de linha dentro vai entre aspas, como o Excel faz, para não virar outra célula ao colar.
Sem seleção nenhuma, copia a célula atual.
A margem lê a geometria de cada bloco, em vez de medir uma altura e somá-la.
O defeito: o número ia se descolando do texto conforme se descia. Medido na captura de um arquivo de 25 nomes — os números avançavam exatos 18 px por linha, o texto avançava ~19,5 px:
| linha | número (y) | texto (y) | distância |
|---|---|---|---|
| 1 | 12 | 11 | alinhado |
| 10 | 174 | 173 | alinhado |
| 25 | 426 | 463 | 37 px — duas linhas |
E a última linha ficava sem número nenhum, porque a soma já tinha passado do fim da área de desenho antes de chegar nela.
A causa era uma linha de código: a altura era medida uma vez, no primeiro bloco visível, e somada a cada volta do laço. Qualquer bloco que meça diferente desloca todos os números seguintes, e o erro se acumula para baixo. E blocos medem diferente por vários motivos — uma linha que quebra ocupa várias, o último bloco carrega a margem inferior do documento, e com a escala de tela do Windows a altura vira fracionária e o arredondamento cobra o seu a cada linha.
Perguntar ao Qt onde cada bloco está custa uma chamada por linha visível —
algumas dezenas — e não erra nunca. O teste força blocos de alturas
[18, 180, 18, 18, 22] e mede: o método antigo erra até 162 px ali.
Quando uma conversão de codificação é recusada, a mensagem traz o nome Unicode e o ponto de código, e não só o caractere.
Veio de um caso real. Um CSV foi recusado para Latin-1 por causa de um Ę —
E com ogonek, letra polonesa — e a mensagem dizia exatamente isso. Estava
certa. Mas na fonte do diálogo Ę e É são indistinguíveis, e a conclusão
natural de quem leu foi que o programa estava errado, porque É obviamente
existe em Latin-1.
Mostrar o caractere não basta quando o problema é justamente que ele se parece
com outro. LATIN CAPITAL LETTER E WITH OGONEK não se confunde com
LATIN CAPITAL LETTER E WITH ACUTE em fonte nenhuma.
Aquele CSV tinha 29 letras do Leste Europeu no lugar de acentos portugueses —
Ę→Ê, ă→ã, ő→õ, ę→ê. São o mesmo byte lido na tabela errada:
0xCA é Ê em CP1252 e Ę em CP1250. O arquivo passou por um programa que o
leu como Centro-Europeu e gravou como UTF-8.
O conserto é texto.encode("cp1250").decode("cp1252"), e depois dele não sobra
um único caractere fora de Latin-1. Está guardado em teste_conversao.py.
Um erro de sintaxe num arquivo de teste faz a suíte inteira não rodar: zero
verificação executada, zero falha contada — e a linha TOTAL dizia
0 falhas. O código de saída já era 1, mas é a linha TOTAL que se lê.
Aconteceu de verdade e passou despercebido. Agora ela nomeia as suítes que não
rodaram.
Um "Sem título" com texto digitado não pergunta nada ao fechar o programa. Ele é guardado, e reaparece na partida seguinte com o mesmo nome — e ainda como rascunho, de modo que o próximo Ctrl+S continua perguntando onde salvar de verdade, em vez de gravar escondido numa pasta interna.
O que tornava isso possível já existia desde a v0.10.0: um documento novo nasce como arquivo de verdade numa pasta interna, e por isso passa pelo mesmo caminho de um arquivo de 1 GB. Faltava uma coisa: ele nascia com zero byte. O texto digitado vive na tabela de peças, não no disco. Fechar sem perguntar e sem guardar antes perderia tudo em silêncio — pior que a caixa de diálogo. "Manter sem salvar" só é seguro se houver onde manter.
Nenhum dos dois pergunta. A diferença está no destino, e é a leitura natural do gesto: fechar a janela guarda tudo para a próxima partida; fechar aquela aba no X é dizer "não quero mais este documento", e guardá-lo para reabrir sozinho depois seria devolver o que a pessoa acabou de dispensar. O rodapé diz o que aconteceu.
Rascunhos órfãos de um fechamento anormal são apagados depois de 7 dias, e essa limpeza roda antes da restauração no arranque. Sem uma proteção, um documento novo deixado aberto duas semanas seria apagado instantes antes de voltar — e a aba simplesmente não reapareceria, sem erro nenhum na tela. Justamente o rascunho mais valioso: o que a pessoa manteve por mais tempo.
Agora a limpeza consulta a sessão. O teste põe dois rascunhos de 30 dias na pasta, um prometido pela sessão e outro não: sobrevive quem a sessão promete reabrir, e a idade não tem voto.
Vão para o fim e o começo do documento, deslizando a janela viva.
Eram da mesma família do Ctrl+Z, e escaparam pelo mesmo motivo: o
QPlainTextEdit só conhece a fatia. "Fim do documento", para ele, é o fim
das 5000 linhas que tem na mão — num arquivo de 200 mil linhas, um lugar
qualquer no meio. A tecla sempre funcionou; o documento dela é que era o errado.
O Ctrl+Home tinha o mesmo defeito e era mais difícil de notar: a fatia do arranque começa na linha 0, então ele acertava por acidente até a primeira vez que a fatia deslizava.
O teste manda a tecla, e não o método — é a lição que o Ctrl+Z deixou, e a sabotagem confirma: sem a correção, o cursor para na linha 5000.
Formatar, abrir planilha e comparar não recusam mais por tamanho. Os três tetos viraram configuração com zero = sem limite, e zero é o padrão de fábrica:
| antes | agora | |
|---|---|---|
| Formatar | 64 MB, constante no código | sem limite |
Planilha .xlsx | 100 MB | sem limite |
| Comparar | 500 mil linhas | sem limite |
O custo continua real — cada uma dessas operações traz o conteúdo inteiro para
a memória, e não existe versão em streaming delas. O que mudou é de quem é a
decisão. Quem quiser a proteção de volta põe um número em
Configurações → Limites, e o mecanismo inteiro volta a valer: o comando fica
desabilitado com o motivo na dica, e a recusa diz onde mudar.
O teto de formatar precisou atravessar até o motor. de_json e de_xml
conferem o tamanho por dentro; liberar só o menu deixaria o comando habilitado
e a recusa apareceria depois do clique — o pior dos dois mundos.
E o 0 de limite_planilha_mb significava "nunca abrir como planilha" —
o oposto de "sem limite". Era assim que a janela reabria um .xlsx inválido
como texto. Virou parâmetro do construtor da aba, que é o que sempre foi: uma
decisão daquela aba, e não uma preferência. Sem isso, a recuperação de um
arquivo corrompido viraria um laço.
Tirar o teto de formatar só serve se o arquivo abrir. E um JSON minificado — exatamente o arquivo que se abre para mandar formatar — é uma linha só. Medido: 271 KB numa linha levavam 42 segundos para abrir; 1,7 MB não terminavam. Um arquivo de 2,3 MB repartido em linhas abria em 0,2 s. O tamanho nunca foi o problema; a linha era.
A causa foi uma regressão introduzida na v0.14.0 junto com o aviso "parece uma
tabela": ele passou a chamar a detecção de dialeto em todo arquivo ao fim
da varredura, e o csv.Sniffer do Python roda um re.findall cujo custo
explode em linha longa. Antes, isso só rodava ao abrir o menu Visualizar.
A amostra passou a truncar cada linha em 4096 caracteres. Não piora detecção nenhuma — um separador de CSV se identifica nos primeiros campos, não no fim da linha, e há teste com um CSV de linhas longas provando que ele continua sendo reconhecido. 42,7 s → 0,07 s, e o arquivo de 1,7 MB passou a abrir em 0,40 s.
De quebra: um arquivo de uma linha só não é mais considerado tabela. O princípio do módulo é escolher o separador pela consistência entre as linhas, e com uma linha apenas toda contagem é trivialmente consistente — um JSON minificado saía de lá como "tabela de 393 colunas, confiança 100". A escolha manual do separador continua funcionando numa linha só: a dúvida era da heurística, não sua.
O .ico está embutido no .exe pelo PyInstaller desde a primeira versão --
é por isso que o Explorer sempre mostrou o ícone certo. Mas o recurso do
executável só vale para o shell: a janela que o Qt cria nasce sem ícone até
alguém chamar setWindowIcon, e ninguém chamava. Barra de título e barra de
tarefas ficavam com o genérico, num programa cujo arquivo no disco tinha o
ícone certo -- e é justamente esse contraste que fez o defeito durar treze
versões.
Havia testes de ícone o tempo todo, e eles conferiam o arquivo: decodificam
o .ico, medem a espessura dos traços, contam os pixels opacos a 16 px.
Nenhum perguntava se a janela usava. Agora há.
No Windows há uma segunda metade: setWindowIcon sozinho não basta. Sem
registrar um AppUserModelID antes da primeira janela, o Windows agrupa a
janela sob o processo que a criou -- python.exe, ao rodar do fonte -- e mostra
o ícone do Python por mais correto que o setWindowIcon esteja. A identidade é
lida na criação da janela: depois não adianta mais.
Um arquivo separado por ;, |, TAB ou outro caractere é ilegível como texto
corrido. Visualizar → Colunas mostra o mesmo arquivo repartido numa grade,
sem carregá-lo na memória: a grade lê da tabela de peças sob demanda, e
editar uma célula troca só os bytes daquele campo.
O separador é reconhecido pela consistência entre as linhas, não pela
frequência do caractere. É o que faz um CSV brasileiro (; separando colunas,
, como decimal) abrir com as colunas certas: a vírgula é frequente, mas só o
; aparece em todas as linhas, cabeçalho incluído.
Visualizar → Colunas com outro separador… deixa escolher o caractere à mão,
com prévia ao vivo: as primeiras linhas já aparecem repartidas enquanto se
escolhe, então dá para ver que o separador está errado sem abrir a grade.
Aceita ; , TAB | : ~ ^ # espaço, ou qualquer caractere digitado --
inclusive \x1f, o separador de unidade do ASCII, que alguns sistemas usam
justamente por nunca ocorrer no dado.
O caso que motiva a tela é o arquivo separado por espaço: o espaço está fora dos candidatos automáticos de propósito, porque com ele na lista toda prosa em português viraria uma tabela de dez colunas. A detecção acerta em recusar, e a escolha manual é o único caminho.
Trocar o separador refaz a grade em vez de remendá-la: o modelo guarda o dialeto e mantém um cache de linhas já repartidas: trocar o atributo na grade viva deixaria metade dos dados repartida pelo separador antigo, sem nada na tela dizendo isso.
Ctrl+F e F3 funcionam dentro da grade: o resultado seleciona a célula,
e a visualização em colunas fica de pé. Antes, procurar jogava de volta para o
modo texto -- desfazendo as colunas exatamente na hora em que elas mais servem,
que é a de conferir o valor achado.
A conversão não é óbvia. A busca devolve (linha, coluna de caractere), e
na grade isso não é uma coluna: o caractere 6 pode estar no primeiro campo ou
no quarto, conforme o tamanho dos anteriores. fatias_de_campos diz onde cada
campo começa e acaba na linha crua, e a ocorrência cai dentro de exatamente um.
F3 também precisou de conta: a grade não tem cursor de caractere, então a
busca seguinte recomeça do início da célula atual mais um. Sem isso, F3
reencontraria a mesma ocorrência para sempre.
Substituir todas também mantém a grade -- ela nunca precisou do modo texto, porque mexe direto na tabela de peças e não no cursor. O que faltava era jogar fora o cache de linhas repartidas depois da troca, senão a grade mostraria os valores de antes. Só o Substituir um a um ainda volta ao texto: ele compara com o que está selecionado no cursor do editor.
No hexadecimal a busca continua voltando ao texto, e com razão: ali a posição é um byte. Mapear caractere para byte exigiria recodificar o prefixo de cada linha, e um multibyte no meio faria a seleção cair no lugar errado sem nenhum erro visível. Voltar avisando é melhor que acertar por sorte.
A grade existia desde a v0.7.0 e estava invisível -- só dentro do menu
Visualizar, que ninguém abre num arquivo de texto. Ao terminar a varredura, um
CSV agora avisa no rodapé que dá para vê-lo em colunas, dizendo qual separador
foi reconhecido. Uma vez por aba, e sem abrir nada sozinho: a segunda vez já
seria cobrança, e trocar a tela de baixo de quem só pediu para ler seria pior.
Configurações → Editor → Número de linha, com três estados:
| Todas | todos os números, com a linha do cursor realçada (padrão) |
| Somente a linha do cursor | visual limpo |
| Nenhum | a margem some, com largura zero |
A margem pintava com a palette() do Qt em vez do tema. Medido: fundo
#f7f7f7 com números #b8b8b8 — 63 de diferença de luminância, contra 83
das cores do tema. E a palette() de um widget não acompanha o tema do editor,
então a margem ficava presa nas cores padrão do Qt em qualquer tema. Só o número
da linha do cursor usava outra cor, e por isso era o único que se
distinguia — parecia um recurso e era uma cor errada. O tema sempre teve
editor.margem_texto e editor.margem_texto_atual, sem ninguém usar.
Arquivo → Configurações (Ctrl+,): tema (escuro, claro, azul ou seguir o
Windows), pasta padrão dos diálogos, número de linha, fonte, e os limites de
leitura.
Duas regras, e as duas estão travadas por teste que varre o fonte — não uma lista à mão, que envelheceria:
tema era lido com um padrão
embutido e nunca declarado; como só o declarado chega ao arquivo de
configuração, não havia como mudar o tema. Eram cinco chaves nessa
situação.limite_de_substituicoes era o inverso:
declarado, editável, e ignorado — o código usava uma constante. Uma opção que
não faz nada é pior que a ausência dela, porque tira a chance de a pessoa
procurar outro caminho.O que dá para aplicar na hora, aplica na hora. Tema, número de linha, fonte e quebra de linha valem nos arquivos já abertos ao fechar a janela — se só valessem no próximo arquivo, a conclusão natural seria que a opção não funciona. Os limites de leitura já foram usados na abertura, e a própria tela avisa isso.
O tema azul é derivado do escuro, e não escrito do zero: só as superfícies
mudam de matiz. Os 40 papéis de realce passam intactos — eles já foram
escolhidos para ter contraste em fundo escuro, e um tema digitado à mão
esqueceria alguns (e tema.cor só avisa no log, então o esquecimento viraria um
realce monocromático sem erro visível).
O programa nunca abre numa janela vazia: sem arquivo na linha de comando e sem
sessão a restaurar, ele cria um Sem título 1 pronto para digitar. Ctrl+N
cria outro a qualquer momento.
O documento novo é um arquivo de verdade, vazio, numa pasta interna
(%APPDATA%\TextForgeEdit ascunhos). Não é detalhe de implementação — é o que
faz ele passar exatamente pelo mesmo código de um arquivo de 1 GB: mesma tabela
de peças, mesmo desfazer, mesma gravação por streaming. Um caminho paralelo "sem
arquivo" seria uma segunda implementação de tudo isso, e as duas divergiriam na
primeira correção feita só numa delas.
Três consequências, e as três estão travadas por teste:
Ctrl+S pergunta onde salvar. Gravar no arquivo de trabalho esconderia o
texto numa pasta interna, e você nunca mais o encontraria.Dois, e não um — são coisas diferentes, e o Explorer as mostra em lugares diferentes:
icone.ico o APLICATIVO quadrado cheio, na barra de tarefas
icone_arquivo.ico o TIPO DE ARQUIVO página com canto dobrado, no Explorer
Com um ícone só, um .txt associado fica com cara de programa na pasta, e
não dá para saber olhando se aquilo é o editor ou um arquivo dele.
Mesma família visual do TextForge — fundo ardósia, barras de texto — com o vinco lateral em azul no lugar do laranja-brasa.
Tudo é grosso, e isso não é escolha estética. A primeira versão tinha um vinco de 1 px e três linhas de 1 px sobre um fundo quase preto: no papel parecia elegante, na barra de tarefas virou um borrão. Duas lições ficaram, e os testes guardam as duas:
#1E1F22 numa barra de tarefas escura tem quase a cor da barra.
O que separa o ícone do fundo é o vinco azul — por isso ele é largo.A página do ícone de arquivo é clara pelo motivo espelhado: ela vive no Explorer, onde o fundo é branco, e por isso ganha borda — sem ela a silhueta de documento desaparece.
ferramentas/gerar_icone.py regera os dois sem Pillow: os .ico são
versionados justamente para o build funcionar numa máquina que não o tenha, e um
gerador que exigisse Pillow anularia isso.
.ssociar.ps1 .txt .csv .log .dat
Escreve apenas em HKCU — sem administrador, nada fora do seu perfil. Usa
OpenWithProgids, que acrescenta o TextForgeEdit à lista "Abrir com" sem
roubar o programa padrão de ninguém. Para trocar de editor em vez de acumular
os dois, -TirarDaLista TextForge.exe. Para desfazer, -Remover.
O programa padrão não sai daqui, e não é limitação do script. Desde o
Windows 10 o UserChoice de cada extensão é protegido por um hash amarrado ao
seu usuário, à extensão e ao horário: escrever ali direto é revertido pelo
sistema. Para tornar o TextForgeEdit padrão, é uma vez por extensão em
Abrir com → Escolher outro aplicativo → Sempre.
Duas armadilhas do PowerShell estão travadas por teste, porque as duas quebram o script em silêncio:
ValueFromRemainingArguments fica fora da ligação
posicional. Sem PositionalBinding = $false, .ssociar.ps1 .txt entende
.txt como o caminho do executável — e o script responde "não encontrei o
.exe" sem dar pista do porquê.*, e o provedor de registro do
PowerShell trata isso como curinga: sem -LiteralPath ele varre as
milhares de chaves de Software\Classes e o script parece travado.Menu Formatar: documento (Shift+Alt+F), seleção, compactar e validar
sintaxe. Cinco motores — JSON, XML, CSS, HTML e SQL.
O Python ficou de fora de propósito. O motor dele é o black, que arrasta
click, pathspec e platformdirs, e um arquivo-fonte Python não é o motivo
deste editor existir. Não é esquecimento; está registrado no código.
Como o resultado chega ao arquivo. Os formatadores recebem str e devolvem
str — não existe versão em streaming disso, e nem daria: indentar exige
conhecer a estrutura inteira. E o editor só tem a fatia. Então o resultado não
passa pelo editor: vai direto à tabela de peças, aparado por
_prefixo_comum/_sufixo_comum — os mesmos da janela viva.
O aparo dá de graça uma propriedade valiosa: reformatar um arquivo já formatado é no-op. Não mexe na tabela, não suja a aba, não muda a data do arquivo ao salvar. E o documento inteiro formatado é um único passo de desfazer.
Acima de 64 MB o comando vem desabilitado com o motivo na dica — formatar é a única operação daqui que precisa do arquivo inteiro na memória, e esse é o limite honesto da técnica. Formatar seleção não tem teto: a seleção está na fatia.
A linguagem e a codificação aparecem no rodapé, e clicar em qualquer um dos dois abre o menu para trocar — é onde a pessoa já está olhando para saber o que o arquivo é. Obrigá-la a subir até a barra de menus para mudar o que está lendo ali é atravessar a janela por uma informação que estava debaixo do cursor.
Os dois campos têm cursor de mãozinha e dica: um rótulo de barra de status normalmente não faz nada ao ser clicado, então sem essa pista ninguém tentaria.
O menu Codificação tem duas coisas que costumam ser confundidas, e a confusão é o defeito clássico aqui:
| O que faz | O arquivo no disco | |
|---|---|---|
| Reinterpretar como | lê os mesmos bytes de outro jeito | não muda |
| Converter para | reescreve o arquivo na codificação escolhida | todo byte muda |
"Abri e veio tudo com acento quebrado" é caso de reinterpretar: o arquivo estava certo, a leitura é que errou. "Preciso mandar este export do ERP em UTF-8" é caso de converter.
Converter é caro aqui, por construção. A gravação normal copia os trechos
intactos byte a byte, direto do mmap — é o que faz salvar 240 MB com três
parágrafos alterados custar segundos. Não existe conversão que preserve os
bytes: á em ISO-8859-1 é um byte e em UTF-8 são dois. O arquivo inteiro passa
por decodificar e recodificar. Continua O(1) em memória, por streaming, mas
é O(n) em trabalho — e o programa avisa antes de começar.
Nada se perde em silêncio. errors="replace" seria fácil e gravaria ? no
lugar de um 中 que não cabe em ISO-8859-1 — sem volta. A conversão para,
diz qual caractere e em que linha, e o arquivo continua exatamente como estava
(a escrita é num temporário ao lado, como toda gravação daqui).
O detalhe que só aparece em arquivo grande: os blocos têm 4 MB e não respeitam
fronteira de caractere, então um ç pode ter um byte no fim de um bloco e o
outro no começo do próximo. Por isso os codecs são incrementais — decodificar
bloco a bloco produziria lixo a cada 4 MB.
O motor veio inteiro do TextForge — o Pintor, as regras e os 24 provedores de
linguagem. O que não veio de graça é o ponto em que os dois programas
diferem:
no TextForge o QTextDocument tem o ARQUIVO;
aqui ele tem uma FATIA tirada do meio de um arquivo de 1 GB.
O QSyntaxHighlighter começa o bloco 0 no contexto inicial da linguagem. Lá isso
está certo por construção. Aqui, uma fatia que caia dentro de um /* comentário */
aberto 3.000 linhas antes seria pintada como código — cores erradas
justamente no trecho que a pessoa foi ler.
A correção é a semente de contexto: antes de pintar, o editor roda a máquina de contextos sobre as 200 linhas anteriores à fatia (sem pintar nada) e usa o resultado como estado de entrada do bloco 0.
linha 200 /* abre o comentário <- fora da fatia
... (as 200 linhas de contexto)
────────── começo da fatia ──────────
linha 350 ainda dentro do comentário <- pintado como comentário, e não como código
São 200 linhas, e não o arquivo inteiro: varrer 500 mil linhas para descobrir a cor da primeira linha da fatia trocaria uma imprecisão visual por uma pausa a cada rolagem. O que não couber nelas volta ao contexto inicial da linguagem — o comportamento de antes.
A suíte teste_realce.py não verifica só que o realce funciona: ela desliga a
semente e exige que o resultado mude. Sem essa contraprova o teste passaria com
o recurso quebrado.
O editor é um QPlainTextEdit de verdade segurando apenas uma fatia:
arquivo de 1 GB
│
├─ linhas 0 .. 1.199.999 na tabela de peças, no disco
├─ linhas 1.200.000 .. 1.205.000 ← JANELA VIVA, num QTextDocument real
└─ linhas 1.205.001 .. fim na tabela de peças, no disco
Rolar para fora faz a fatia deslizar: o que estava vivo volta para a tabela de peças e uma fatia nova é carregada. Dentro dela funciona tudo o que o Qt sabe fazer — caret, acentuação com tecla morta, seleção, clipboard, arrastar.
A escrita de volta é mínima: o prefixo e o sufixo iguais são descartados, e só o miolo que mudou entra na tabela. Sem isso, cada deslize com uma vírgula corrigida injetaria a fatia inteira — centenas de KB para representar um byte.
Ctrl+Z consulta duas pilhas, nesta ordem:
Substituir todas.Uma substituição em massa cabe num único Ctrl+Z: as trocas entram como um
grupo, e desfazer processa o grupo inteiro. Sem isso, voltar atrás de 500
substituições exigiria 500 Ctrl+Z, o que na prática é não poder voltar.
O Ctrl+Z é interceptado no keyPressEvent do editor, e não deixado a cargo do
atalho do menu: o QPlainTextEdit fica com essa tecla antes do menu e desfaria
na própria pilha — vazia depois de todo deslize.
Limitação que resta: ao deslizar a fatia, o que foi editado é consolidado na
tabela e a pilha do Qt recomeça. O desfazer continua funcionando (pela tabela),
mas a granularidade fica mais grossa — um Ctrl+Z desfaz a consolidação inteira
daquele trecho, e não a última tecla.
| Abas | uma por arquivo — reabrir o mesmo caminho foca a aba existente, inclusive com caixa diferente |
| Salvar / Salvar como / Salvar tudo | Ctrl+S, Ctrl+Shift+S |
| Localizar e substituir | Ctrl+F localiza, Ctrl+H substitui, F3 e Shift+F3 navegam — com maiúsculas, palavra inteira e regex |
| Arrastar-e-soltar | solte arquivos na janela |
| Ir para linha | Ctrl+G |
A busca varre o documento inteiro, e não a fatia carregada. Achar na linha 150.000 desliza a janela até lá. Ela também enxerga o que você digitou e ainda não gravou — e não encontra o que você apagou, embora ainda esteja no arquivo.
Substituir todas aplica de trás para a frente: do começo, cada troca deslocaria
o que vem depois e a segunda cairia no lugar errado. Tem teto de 100.000 por
passada, avisado antes.
O log fica em %APPDATA%\TextForgeEdit\textforgeedit.log, com rotação em 2 MB.
Toda exceção não tratada vai para lá e para erro.log, com o traceback
inteiro.
Isso existe por uma falha concreta: numa sessão de teste o executável sumiu depois de gravar um arquivo de 176 MB e não havia nada para consultar. Três tentativas de reproduzir falharam, e o defeito segue sem causa conhecida — mas na próxima vez haverá rastro.
Tudo é em bytes, não em caracteres. Saber que a posição 4.000.000 é o caractere 3.812.577 exigiria decodificar tudo antes dela — ou seja, carregar o arquivo. A decodificação acontece linha a linha, na hora de desenhar, e uma linha é curta.
Editar exige o índice completo. Partir uma peça precisa saber quantas linhas ficam de cada lado. Enquanto a varredura corre, o arquivo é navegável e a contagem de linhas cresce na tela — mas não se edita.
Digitação seguida vira uma peça só. Sem esse caminho rápido, digitar um
parágrafo criaria centenas de peças. E as teclas seguidas viram uma operação
de desfazer: Ctrl+Z desfaz a frase, não a letra.
A ordem da gravação no Windows não é negociável: escrever o temporário com o mmap aberto → fechar o mmap → trocar → reabrir. Fechar antes grava arquivo vazio; deixar aberto faz a troca falhar com acesso negado.
py -3.13 -m venv .venv
.venv\Scripts\python.exe -m pip install -r requirements.txt
.venv\Scripts\python.exe tests\rodar_todos.py
build.bat :: dist\TextForgeEdit\ (one-dir, recomendado)
build.bat umarquivo :: dist\TextForgeEdit.exe (portátil)
O build.bat roda a suíte antes de empacotar e uma prova de vida do .exe
depois: ela cria um arquivo, edita, grava, confere byte a byte e ainda testa
a busca. Excludes agressivos quebram o programa só em tempo de execução — sem
essa prova, isso chegaria como relatório de bug do usuário.
Sem pytest, de propósito — cada suíte roda num processo separado, então um travamento de Qt numa não leva as outras.
MIT. Autor: Ricardo Biazin.