diff --git a/CHANGELOG.md b/CHANGELOG.md
index 65b0d8d..6a88260 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,28 @@ Todas as mudanças relevantes do projeto. Formato baseado em
[Keep a Changelog](https://keepachangelog.com/pt-BR/1.1.0/); versões seguem
[SemVer](https://semver.org/lang/pt-BR/). Próximos passos ficam no [roadmap](BACKLOG.md).
+## [2.7.1] — 2026-10-18
+
+### Corrigido
+- **Botões que sumiam ao redimensionar:** na janela *Espaço por pasta* os botões da direita ("Abrir no
+ sistema", "Usar como Pasta Raiz", "Ocultar treemap") desapareciam abaixo de ~1220 px de largura, e na
+ janela principal "Apagar índice" ficava cortado no tamanho mínimo. A barra de botões da janela de espaço
+ agora tem **duas linhas** e o **tamanho mínimo das duas janelas é calculado pelo conteúdo** (fonte e
+ escala do sistema), nunca passando da tela.
+- Barras de progresso paradas pareciam "meio cheias": agora só aparecem enquanto trabalham.
+
+### Alterado
+- Janela principal abre mais alta (mais espaço para a lista de resultados); botão renomeado para
+ **Espaço e treemap...**; a métrica do treemap virou **Blocos por: Tamanho / Quantidade**.
+
+### Adicionado
+- Teste automático de layout (`TestResponsiveLayout`): reduz e amplia as duas janelas e confere que nenhum
+ botão fica escondido, cortado ou fora da janela.
+- `tools/screenshots.py`: gera as imagens do README a partir de um acervo **fictício** gravado direto no
+ índice (nada do computador de quem roda aparece).
+- README reescrito com imagens (busca, treemap por tamanho e por quantidade, HTML exportado, tamanho
+ mínimo das janelas, CLI em texto e JSON).
+
## [2.7.0] — 2026-10-17
### Adicionado
diff --git a/CITATION.cff b/CITATION.cff
index 911621e..e897d31 100644
--- a/CITATION.cff
+++ b/CITATION.cff
@@ -11,8 +11,8 @@ abstract: >-
authors:
- family-names: Gambati
given-names: Joberth Firmino
-version: 2.7.0
-date-released: 2026-10-17
+version: 2.7.1
+date-released: 2026-10-18
license: MIT
repository-code: "https://github.com/Yiuky/ReperIO"
url: "https://github.com/Yiuky/ReperIO"
diff --git a/README.md b/README.md
index c090a9f..72ba14b 100644
--- a/README.md
+++ b/README.md
@@ -6,20 +6,21 @@
Encontre, indexe, exporte e copie arquivos em servidores de arquivos grandes e lentos, sem instalar nada
- Busca por nome e extensão · Índice local · Excel / CSV / TXT · Cópia paralela · SMB, DFS e unidades mapeadas · Windows e Linux
+ Busca por nome e extensão · Treemap interativo · Índice local · CLI com JSON para IAs · Excel / CSV / TXT · Cópia paralela · SMB, DFS e unidades mapeadas · Windows e Linux
-
+
⬇️ Baixar •
+ 🖼️ Veja em ação •
📖 Como usar •
📋 Novidades •
🐞 Relatar problema •
@@ -30,23 +31,102 @@
---
-
+
## 📌 O que faz
-O ReperIO varre pastas locais e **servidores de arquivos de rede** (SMB/CIFS, DFS, unidades mapeadas)
-atrás de arquivos por **nome, extensão e tamanho**: centenas de milhares de pastas, pastas com dezenas de
-milhares de arquivos e servidores que às vezes param de responder. Os achados formam o seu
-**repertório**: você pode filtrar, inspecionar o **espaço ocupado por pasta**, ordenar, **exportar para
-Excel, CSV ou TXT** e **copiar em lote**.
+O ReperIO responde a três perguntas sobre pastas gigantes, locais ou em **servidor de arquivos de rede**
+(SMB/CIFS, DFS, unidades mapeadas):
-Foi criado para bancos de imagens e dados geográficos (GIS), mas serve a qualquer acervo grande de
-arquivos em rede ou disco local.
+1. **Onde está o arquivo?** Busca por nome, extensão e tamanho em centenas de milhares de pastas,
+ mesmo com servidores que às vezes param de responder.
+2. **Onde foi parar o espaço?** Tamanho de cada pasta e subpasta, com **treemap interativo** (estilo
+ SpaceSniffer) por tamanho ou por quantidade de arquivos.
+3. **Como automatizar?** Linha de comando com **saída JSON estável**, pronta para scripts, tarefas
+ agendadas e **IAs**.
+
+Os achados formam o seu **repertório**: filtre, ordene, **exporte para Excel, CSV ou TXT** e **copie
+em lote**. Foi criado para bancos de imagens e dados geográficos (GIS), mas serve a qualquer acervo grande.
> Projeto **pessoal e independente** de Joberth Firmino Gambati: não é um produto oficial de nenhuma
> instituição nem fala em nome dela.
+## 🖼️ Veja em ação
+
+Todas as imagens abaixo usam um acervo **fictício** (≈ 129 GB, 42 mil arquivos), gerado por
+[`tools/screenshots.py`](tools/screenshots.py).
+
+### Busca e filtros
+
+
+
+
+
+Digite o padrão, filtre por extensão, tamanho e pastas ignoradas e clique em **Iniciar busca**. Os
+resultados aparecem enquanto a busca acontece; o contador mostra o volume encontrado e o selecionado.
+
+### Espaço por pasta com treemap interativo
+
+
+
+
+
+**Espaço e treemap...** abre um painel com o **treemap acima da tabela, na mesma janela**. A área de cada
+bloco é proporcional ao **tamanho**: aqui, `VIDEOS_DRONE` ocupa metade do acervo.
+
+
+
+
+
+Em **Blocos por: Quantidade** o mesmo acervo muda de cara: `TILES_WEB` tem 42 mil arquivinhos e pesa
+menos de 1 GB. É o tipo de pasta que deixa a busca, a cópia e o backup lentos sem ocupar espaço.
+
+| Ação | Resultado |
+|---|---|
+| **Passar o mouse** | Destaca o bloco e mostra caminho, tamanho, arquivos e participação |
+| **Clicar** | Seleciona o bloco; a linha da tabela acompanha (e vice-versa) |
+| **Duplo clique** ou **roda do mouse para cima** | Entra na pasta sob o mouse |
+| **Roda para baixo**, **botão direito** ou **Backspace** | Sobe um nível |
+| **Blocos por: Tamanho / Quantidade** | Troca a métrica |
+| **Ocultar treemap** | Esconde o painel e deixa só a tabela |
+| **Exportar... → Treemap (página HTML)** | Grava uma página autônoma, sem internet |
+
+A página HTML (também gerada por `--treemap mapa.html`) funciona em qualquer navegador, tem tema claro e
+escuro e alterna entre tamanho e quantidade:
+
+
+
+
+
+### Fluido e responsivo
+
+A interface nunca trava (leitura de disco e rede ficam fora da thread da janela), o treemap se
+redesenha ao redimensionar, e **nenhum botão some ao reduzir a janela**: o tamanho mínimo é calculado
+a partir do que os controles realmente pedem (fonte e escala do sistema), e um teste automático
+confere isso em vários tamanhos.
+
+
+
+
+
+
+As duas janelas no menor tamanho permitido: tudo continua visível e clicável.
+
+### Linha de comando e IAs
+
+
+
+
+
+Com `--json`, a CLI entrega **um único objeto JSON** no stdout (mensagens vão para o stderr), com
+campos estáveis, códigos de saída fixos e `--manifesto` para uma IA descobrir tudo sozinha:
+
+
+
+
+Trecho simplificado: a saída real traz mais campos (veja docs/CLI.md).
+
## 💡 Por que "ReperIO"
Do latim ***reperire***, "achar, descobrir, trazer à luz", a mesma raiz de ***repertório***
@@ -59,10 +139,13 @@ faz: **acha** os arquivos e compõe o **repertório** do que foi encontrado. O *
| | |
|---|---|
| 🔎 **Busca e filtros** | "Contém" (`IMAGENS_`) ou curingas (`*.shp`, `LC08_*_B4.TIF`), extensões, **tamanho** (`> 100 MB`, `10MB..500MB`) e **exclusão de pastas** (`_backup, node_modules`) |
-| 📊 **Espaço por pasta** | Mostra quanto espaço cada pasta e subpasta ocupa (com `%`, quantidade de arquivos, navegação com duplo clique, pausa/retomada e exportação) |
-| ⚡ **Rápido em pastas enormes** | Leitura em paralelo e em lotes grandes por ida ao servidor (~3× mais rápido em pastas com 65 mil arquivos) |
-| 🗂️ **Índice local e compartilhável** | A 2ª busca na mesma pasta sai em segundos, com qualquer padrão e até com o servidor fora do ar; suporta indexação agendada via CLI (`--indexar`) |
+| 🗺️ **Treemap interativo** | Blocos proporcionais ao **tamanho** ou à **quantidade de arquivos**, com dica ao passar o mouse, seleção sincronizada com a tabela e zoom pela roda do mouse; exporta para HTML |
+| 📊 **Espaço por pasta** | Quanto cada pasta e subpasta ocupa (com `%`, quantidade de arquivos, navegação, pausa/retomada do mapeamento e exportação) |
+| 🤖 **CLI para IAs e scripts** | `--json` estável, `--so-indice` (nunca varre o disco), `--manifesto`, códigos de saída fixos; ações `--espaco`, `--maiores`, `--extensoes`, `--buscar`, `--treemap`, `--status` |
+| ⚡ **Rápido em pastas enormes** | Leitura em paralelo e em lotes grandes por ida ao servidor (~3× mais rápido em pastas com 65 mil arquivos); 869 mil arquivos indexados em ~85 s ([benchmark](docs/BENCHMARK.md)) |
+| 🗂️ **Índice local e compartilhável** | A 2ª busca na mesma pasta sai em segundos, com qualquer padrão e até com o servidor fora do ar; indexação agendada via CLI (`--indexar`) |
| 🛡️ **Não trava** | Uma pasta que o servidor não consegue abrir não segura a busca: o ReperIO troca de conexão e trata a pasta à parte |
+| 📐 **Responsivo** | Nenhum botão some ao redimensionar; tamanho mínimo calculado pelo conteúdo e coberto por teste automático |
| 📤 **Exportar** | Resultados e relatórios de espaço para **Excel (.xlsx)**, **CSV** ou **TXT**, sem precisar do Excel para gerar |
| 📋 **Cópia paralela** | 4 cópias simultâneas, em blocos de 1 MB, com cancelamento a qualquer momento e nomes repetidos renomeados |
| 💻 **Portátil** | Executável único para Windows e Linux; sem instalação, sem administrador, sem `pip` |
@@ -75,6 +158,9 @@ faz: **acha** os arquivos e compõe o **repertório** do que foi encontrado. O *
| **Linux x64** | `./ReperIO` | glibc 2.35+ (Ubuntu 22.04+, Debian 12+, Fedora 36+…) |
| **macOS e outros** | [código-fonte](#-rodar-a-partir-do-código-fonte) | Python 3.8+ com tkinter |
+A CLI (`--json`, `--espaco`, `--buscar`…) roda só com **Python 3.8+**, sem tkinter, inclusive em servidores
+sem interface gráfica.
+
## ⚡ Instalação
1. Baixe o `.zip` do seu sistema em **[Releases](https://github.com/Yiuky/ReperIO/releases/latest)**:
@@ -106,8 +192,7 @@ Linux); caso contrário, abre o executável. Nos dois casos, nada é baixado nem
novo troca o padrão sem perder o que já foi lido.
7. Na lista de resultados:
- **Duplo clique** em um arquivo abre a pasta dele no sistema já destacando o arquivo;
- - **Espaço por pasta...** abre o painel interativo com o consumo de espaço de cada subpasta (com
- duplo clique para entrar nas subpastas, botão *Mapear pasta agora* com pausa/retomada e exportação);
+ - **Espaço e treemap...** abre o painel de [espaço por pasta com treemap](#espaço-por-pasta-com-treemap-interativo);
- **Exportar...** salva a lista em Excel, CSV ou TXT; ou
- informe o **Destino** e clique **Copiar selecionados**. *Plana* coloca tudo numa pasta;
*Manter estrutura original* reproduz as subpastas.
@@ -149,27 +234,30 @@ padrão ou filtro de tamanho. Na busca seguinte:
- **Apagar índice** limpa tudo; desmarque **Usar** para ignorá-lo numa busca.
- Fica em `%LOCALAPPDATA%\reperio\` (Windows) ou `~/.local/state/reperio/` (Linux), ou no caminho
informado com `--indice ` (útil para compartilhar um índice na rede).
-- **Modo sem janela (CLI)** para tarefas agendadas, auditoria rápida de disco e **uso por IAs**:
- ```bash
- python reperio.py --indexar "\\servidor\dados" --indice "\\servidor\dados\_reperio.sqlite"
- python reperio.py --espaco "C:\Users\voce" --top 25 --exportar espaco.xlsx
- python reperio.py --maiores 20 --em D:\ --so-indice # 20 maiores arquivos, sem tocar o disco
- python reperio.py --buscar "*.iso" --em D:\ --tamanho "> 1 GB" --json
- python reperio.py --espaco D:\ --treemap mapa.html # mapa visual em HTML
- ```
- `--json` entrega **um objeto JSON** estável no stdout, `--so-indice` garante respostas instantâneas,
- `--manifesto` descreve a CLI em JSON e os códigos de saída são fixos. Referência em
- [docs/CLI.md](docs/CLI.md); guia para agentes em [docs/IA.md](docs/IA.md); números medidos em
- [docs/BENCHMARK.md](docs/BENCHMARK.md).
-
-### 🗺️ Treemap interativo
-
-Em **Espaço por pasta...**, um painel de **treemap fica acima da tabela, na mesma janela** (estilo
-SpaceSniffer): a **área dos blocos é proporcional ao tamanho ou à quantidade de arquivos** (botão
-**Por: tamanho / quantidade**; **Ocultar treemap** esconde o painel). Passe o mouse para ver os detalhes,
-clique para selecionar (a tabela acompanha), **roda do mouse** ou duplo clique para entrar numa pasta,
-botão direito para subir. **Exportar... → Treemap
-(página HTML)** ou `--treemap mapa.html` gera uma página autônoma (sem internet, tema claro/escuro).
+
+### Modo sem janela (CLI), para scripts e IAs
+
+```bash
+python reperio.py --indexar "\\servidor\dados" --indice "\\servidor\dados\_reperio.sqlite"
+python reperio.py --espaco "C:\Users\voce" --top 25 --exportar espaco.xlsx
+python reperio.py --maiores 20 --em D:\ --so-indice # 20 maiores arquivos, sem tocar o disco
+python reperio.py --extensoes 10 --em D:\ --so-indice # espaço por extensão
+python reperio.py --buscar "*.iso" --em D:\ --tamanho "> 1 GB" --json
+python reperio.py --espaco D:\ --treemap mapa.html # mapa visual em HTML
+python reperio.py --manifesto # a CLI descrevendo a si mesma, em JSON
+```
+
+| Ideia | Como |
+|---|---|
+| Saída para máquinas | `--json`: um objeto JSON em ASCII no stdout; progresso e avisos no stderr |
+| Nunca varrer sem querer | `--so-indice`: sem dados no índice, termina com código **3** |
+| Descobrir sozinha | `--manifesto`: argumentos, campos de saída, códigos e exemplos em JSON |
+| Códigos de saída | `0` ok · `1` erro · `2` uso incorreto · `3` pasta/índice ausente · `130` Ctrl+C |
+| Erro de digitação | Argumento desconhecido é erro de uso: a janela **não** abre por engano |
+
+O ReperIO **só lê**: nenhuma ação da CLI apaga, move ou altera arquivos. Referência completa em
+[docs/CLI.md](docs/CLI.md); guia e receitas para agentes em [docs/IA.md](docs/IA.md); números medidos
+em discos reais em [docs/BENCHMARK.md](docs/BENCHMARK.md).
## 🛡️ Confiável em redes lentas e instáveis
@@ -314,7 +402,7 @@ python -m unittest discover -s tests -v # Windows
xvfb-run -a python3 -m unittest discover -s tests -v # Linux sem tela
```
-95 testes, incluindo a simulação dos cenários vistos em servidor real: pasta que trava a conexão,
+96 testes, incluindo a simulação dos cenários vistos em servidor real: pasta que trava a conexão,
vítimas refeitas na hora, cascata entre conexões, pastas grandes que não são travamento, nomes
inválidos de servidores Samba antigos, filtros de tamanho/exclusão, análise de espaço por pasta, treemap, CLI/JSON e
exportação com datas corrompidas. O GitHub Actions roda a suíte em Windows e Linux, com Python 3.8 e
@@ -346,8 +434,11 @@ do [CITATION.cff](CITATION.cff)) ou assim:
**ReperIO** (from Latin *reperire*, "to find, to discover", the root of *repertory*) is a portable,
dependency-free desktop tool for **Windows and Linux** that searches local folders and **network file
servers** (SMB/CIFS, DFS, mapped drives) for files by name, extension and size, analyzes **disk/folder
-space usage**, builds a local **SQLite index** for instant repeated searches, **exports** results to
-Excel (.xlsx), CSV or TXT, and **copies** files in parallel.
+space usage** with an interactive **treemap** (by size or by file count), builds a local **SQLite index**
+for instant repeated searches, **exports** results to Excel (.xlsx), CSV or TXT, and **copies** files in
+parallel. A headless **CLI with stable JSON output** (`--json`, `--so-indice`, `--manifesto`, fixed exit
+codes) makes it ready for scripts, scheduled tasks and AI agents; the UI never hides a button when the
+window is resized.
It was built for very large, slow and unreliable file servers. On Windows, a single folder that the
server cannot open stalls the whole SMB connection for minutes; ReperIO opens **several independent
diff --git a/docs/CLI.md b/docs/CLI.md
index 41d4ff3..5b425f4 100644
--- a/docs/CLI.md
+++ b/docs/CLI.md
@@ -117,7 +117,7 @@ de primeiro nível. Serve para achar em segundos "o que está comendo o disco".
- **Na janela (painel interativo, estilo SpaceSniffer):** em **Espaço por pasta...** o treemap fica em um
painel **acima da tabela**, na mesma janela (a divisória é ajustável; **Ocultar treemap** esconde o
- painel e **Por: tamanho / quantidade** troca a métrica). Mostra 3 níveis de subpastas aninhadas.
+ painel e **Blocos por: Tamanho / Quantidade** troca a métrica). Mostra 3 níveis de subpastas aninhadas.
- **Passar o mouse:** destaca o bloco e mostra uma dica com caminho, tamanho, arquivos e participação.
- **Clique:** seleciona o bloco e marca a linha correspondente da tabela (e vice-versa: escolher uma
linha da tabela contorna o bloco).
diff --git a/docs/images/cli_json.png b/docs/images/cli_json.png
new file mode 100644
index 0000000..35402be
Binary files /dev/null and b/docs/images/cli_json.png differ
diff --git a/docs/images/cli_texto.png b/docs/images/cli_texto.png
new file mode 100644
index 0000000..ecd557f
Binary files /dev/null and b/docs/images/cli_texto.png differ
diff --git a/docs/images/espaco_tamanho_minimo.png b/docs/images/espaco_tamanho_minimo.png
new file mode 100644
index 0000000..aa2cbbc
Binary files /dev/null and b/docs/images/espaco_tamanho_minimo.png differ
diff --git a/docs/images/espaco_treemap_quantidade.png b/docs/images/espaco_treemap_quantidade.png
new file mode 100644
index 0000000..a42db97
Binary files /dev/null and b/docs/images/espaco_treemap_quantidade.png differ
diff --git a/docs/images/espaco_treemap_tamanho.png b/docs/images/espaco_treemap_tamanho.png
new file mode 100644
index 0000000..cb3b761
Binary files /dev/null and b/docs/images/espaco_treemap_tamanho.png differ
diff --git a/docs/images/janela_principal.png b/docs/images/janela_principal.png
index 90da2a7..632aae6 100644
Binary files a/docs/images/janela_principal.png and b/docs/images/janela_principal.png differ
diff --git a/docs/images/janela_principal_minima.png b/docs/images/janela_principal_minima.png
new file mode 100644
index 0000000..5cedbe8
Binary files /dev/null and b/docs/images/janela_principal_minima.png differ
diff --git a/docs/images/treemap_html.png b/docs/images/treemap_html.png
new file mode 100644
index 0000000..55681d0
Binary files /dev/null and b/docs/images/treemap_html.png differ
diff --git a/reperio.py b/reperio.py
index 6468fd3..70382b5 100644
--- a/reperio.py
+++ b/reperio.py
@@ -36,7 +36,7 @@
import time
import traceback
-__version__ = "2.7.0"
+__version__ = "2.7.1"
APP_NAME = "ReperIO"
APP_TAGLINE = "busca, índice, exportação e cópia de arquivos em servidores"
APP_URL = "https://github.com/Yiuky/ReperIO"
@@ -3108,6 +3108,18 @@ def _build(self):
self._filter(tp)
self._results(mp)
self._copy_panel(bp)
+ # Tamanho mínimo = o que os painéis pedem + um pedaço útil da lista de resultados
+ self.root.update_idletasks()
+ need_w = max(tp.winfo_reqwidth(), mp.winfo_reqwidth(), bp.winfo_reqwidth())
+ need_h = (tp.winfo_reqheight() + bp.winfo_reqheight() + 150
+ + sum(w.winfo_reqheight() for w in self.root.winfo_children()
+ if w.winfo_manager() == "pack" and w is not pane))
+ sw, sh = self.root.winfo_screenwidth(), self.root.winfo_screenheight()
+ self.root.minsize(min(max(900, need_w), sw - 40), min(max(560, need_h), sh - 90))
+ cw, ch = self.root.winfo_width(), self.root.winfo_height()
+ mw, mh = self.root.minsize()
+ if cw < mw or ch < mh:
+ self.root.geometry("%dx%d" % (max(cw, mw), max(ch, mh)))
def _hdr(self):
h = ttk.Frame(self.root, style="Panel.TFrame")
@@ -3828,8 +3840,8 @@ def __init__(self, app, initial_dir=""):
self.app = app
self.win = tk.Toplevel(app.root)
self.win.title("Espaço por pasta — %s" % APP_NAME)
- self.win.geometry("1260x760")
- self.win.minsize(900, 480)
+ self.win.geometry("1200x780")
+ self.win.minsize(720, 480)
self.win.configure(bg=app.BG)
self._closed = False
self._q = queue.Queue()
@@ -3851,6 +3863,11 @@ def __init__(self, app, initial_dir=""):
self._tm_after = None
self._tm_last_zoom = 0.0
self._build(initial_dir)
+ self.win.update_idletasks()
+ fixed = [w for w in self.win.winfo_children() if w.winfo_manager() == "pack" and w is not self.pw]
+ fit_minsize(self.win, 720, 480,
+ need_w=max(w.winfo_reqwidth() for w in fixed) + 8,
+ need_h=sum(w.winfo_reqheight() for w in fixed) + 260) # + treemap e tabela
self.win.protocol("WM_DELETE_WINDOW", self._close)
self._poll()
if initial_dir:
@@ -3893,19 +3910,23 @@ def _build(self, initial_dir):
self.b_export = ttk.Button(bf, text="Exportar...", style="Primary.TButton",
command=self._export)
self.b_export.pack(side="right", padx=(4, 0))
- self.f_metric = ttk.Frame(bf, style="Panel.TFrame")
- self.f_metric.pack(side="right", padx=(4, 8))
+
+ # 2ª linha: visualização (treemap) à esquerda, ações sobre a pasta à direita
+ bf2 = ttk.Frame(top, style="Panel.TFrame")
+ bf2.grid(row=2, column=0, columnspan=3, padx=12, pady=(0, 8), sticky="ew")
+ self.b_tm = ttk.Button(bf2, text="Ocultar treemap", style="Secondary.TButton",
+ command=self._toggle_treemap)
+ self.b_tm.pack(side="left", padx=(0, 4))
+ self.f_metric = ttk.Frame(bf2, style="Panel.TFrame")
+ self.f_metric.pack(side="left", padx=(8, 4))
ttk.Label(self.f_metric, text="Blocos por:", style="Panel.TLabel").pack(side="left", padx=(0, 4))
self.v_metric = tk.StringVar(value="tamanho")
for txt, val in (("Tamanho", "tamanho"), ("Quantidade", "quantidade")):
ttk.Radiobutton(self.f_metric, text=txt, value=val, variable=self.v_metric,
style="Dark.TRadiobutton", command=self._set_metric).pack(side="left", padx=2)
- self.b_tm = ttk.Button(bf, text="Ocultar treemap", style="Secondary.TButton",
- command=self._toggle_treemap)
- self.b_tm.pack(side="right", padx=4)
- ttk.Button(bf, text="Abrir no sistema", style="Secondary.TButton",
- command=self._open_selected).pack(side="right", padx=4)
- ttk.Button(bf, text="Usar como Pasta Raiz", style="Secondary.TButton",
+ ttk.Button(bf2, text="Abrir no sistema", style="Secondary.TButton",
+ command=self._open_selected).pack(side="right", padx=(4, 0))
+ ttk.Button(bf2, text="Usar como Pasta Raiz", style="Secondary.TButton",
command=self._use_as_root).pack(side="right", padx=4)
ttk.Separator(self.win, orient="horizontal").pack(fill="x")
@@ -3916,7 +3937,7 @@ def _build(self, initial_dir):
sf, text=" Informe uma pasta para ver o espaço ocupado por cada subpasta.",
style="Counter.TLabel")
self.lbl_sum.pack(side="left", padx=8, pady=5)
- ttk.Label(sf, text="Cor = pasta de 1º nível · cinza = arquivos soltos · roda do mouse entra/sobe · botão direito sobe ",
+ ttk.Label(sf, text="Roda do mouse: entra/sobe · botão direito: sobe ",
style="Sub.TLabel").pack(side="right", padx=8, pady=5)
self.pw = ttk.PanedWindow(self.win, orient="vertical")
@@ -4100,7 +4121,7 @@ def _toggle_treemap(self):
if self._tm_on:
self.pw.insert(0, self._tm_frame, weight=3)
self.b_tm.config(text="Ocultar treemap")
- self.f_metric.pack(side="right", padx=(4, 8))
+ self.f_metric.pack(side="left", padx=(8, 4))
self._tm_schedule()
else:
self.pw.forget(self._tm_frame)
@@ -4478,6 +4499,24 @@ def _close(self):
pass
# ===========================================================================
+def fit_minsize(win, min_w, min_h, need_w=None, need_h=None):
+ """
+ Define o tamanho mínimo da janela a partir do que os controles realmente pedem
+ (fontes e DPI mudam de máquina para máquina), para que nenhum botão fique
+ cortado ou escondido ao reduzir a janela. Nunca passa da tela. Devolve (w, h).
+ `need_w`/`need_h`: o que os controles fixos pedem (padrão: o pedido da janela toda).
+ """
+ win.update_idletasks()
+ w = max(min_w, win.winfo_reqwidth() if need_w is None else need_w)
+ h = max(min_h, win.winfo_reqheight() if need_h is None else need_h)
+ w = min(w, max(min_w, win.winfo_screenwidth() - 40))
+ h = min(h, max(min_h, win.winfo_screenheight() - 90))
+ win.minsize(w, h)
+ cw, ch = win.winfo_width(), win.winfo_height()
+ if cw < w or ch < h: # janela já aberta menor que o mínimo: cresce
+ win.geometry("%dx%d" % (max(cw, w), max(ch, h)))
+ return w, h
+
def _hide_own_console():
"""Esconde a janela de console quando ela foi criada SÓ para este processo
(duplo clique no .py com python.exe). Se o script foi chamado de um
diff --git a/tests/test_cli_treemap.py b/tests/test_cli_treemap.py
index 658b8bc..0c8b51a 100644
--- a/tests/test_cli_treemap.py
+++ b/tests/test_cli_treemap.py
@@ -355,5 +355,82 @@ class Ev:
shutil.rmtree(base, ignore_errors=True)
+def _all_widgets(w):
+ yield w
+ for c in w.winfo_children():
+ yield from _all_widgets(c)
+
+
+def hidden_buttons(top):
+ """Botões/opções que, no tamanho atual, estão cortados, fora da janela ou escondidos."""
+ top.update_idletasks()
+ top.update()
+ W, H = top.winfo_width(), top.winfo_height()
+ bad = []
+ for w in _all_widgets(top):
+ if w.winfo_class() not in ("TButton", "TCheckbutton", "TRadiobutton"):
+ continue
+ name = str(w.cget("text"))
+ if not w.winfo_ismapped():
+ if w.winfo_manager():
+ bad.append((name, "não mapeado"))
+ continue
+ right = w.winfo_rootx() + w.winfo_width()
+ bottom = w.winfo_rooty() + w.winfo_height()
+ if (right > top.winfo_rootx() + W + 1 or bottom > top.winfo_rooty() + H + 1
+ or w.winfo_width() < w.winfo_reqwidth() - 2):
+ bad.append((name, "cortado/fora da janela"))
+ continue
+ anc = w.master
+ while anc is not None and anc is not top: # cortado por um contêiner (painel)
+ if (right > anc.winfo_rootx() + anc.winfo_width() + 1
+ or bottom > anc.winfo_rooty() + anc.winfo_height() + 1):
+ bad.append((name, "cortado pelo contêiner " + anc.winfo_class()))
+ break
+ anc = anc.master
+ return bad
+
+
+class TestResponsiveLayout(unittest.TestCase):
+ """Nenhum botão pode sumir ao redimensionar: o tamanho mínimo vem do conteúdo."""
+
+ def test_buttons_stay_visible_at_any_size(self):
+ if g.tk is None:
+ self.skipTest("tkinter indisponível")
+ try:
+ root = g.tk.Tk()
+ except g.tk.TclError as e:
+ self.skipTest("sem display: %s" % e)
+ base = tempfile.mkdtemp(prefix="rp_resp_")
+ idx = g.Index(os.path.join(base, "idx.sqlite"))
+ try:
+ app = g.App(root, idx)
+ root.update()
+ mw, mh = root.minsize()
+ self.assertGreaterEqual(mw, 900)
+ for w, h in ((mw, mh), (mw + 120, mh + 60), (1100, 820), (1600, 900), (mw, mh + 200)):
+ root.geometry("%dx%d+0+0" % (w, h))
+ self.assertEqual(hidden_buttons(root), [], "janela principal %dx%d" % (w, h))
+ # pedir menos que o mínimo é barrado pelo minsize: continua tudo visível
+ root.geometry("500x300+0+0")
+ self.assertEqual(hidden_buttons(root), [], "janela principal abaixo do mínimo")
+ app._open_space()
+ dlg = app._space_win.win
+ dlg.update()
+ dw, dh = dlg.minsize()
+ for w, h in ((dw, dh), (dw + 80, dh + 40), (1200, 780), (1600, 900), (dw, 900)):
+ dlg.geometry("%dx%d+0+0" % (w, h))
+ self.assertEqual(hidden_buttons(dlg), [], "janela de espaço %dx%d" % (w, h))
+ dlg.geometry("300x200+0+0")
+ self.assertEqual(hidden_buttons(dlg), [], "janela de espaço abaixo do mínimo")
+ app._space_win._close()
+ finally:
+ idx.close()
+ root.destroy()
+ app = root = None
+ gc.collect()
+ shutil.rmtree(base, ignore_errors=True)
+
+
if __name__ == "__main__":
unittest.main()
diff --git a/tools/screenshots.py b/tools/screenshots.py
new file mode 100644
index 0000000..3a7b7be
--- /dev/null
+++ b/tools/screenshots.py
@@ -0,0 +1,294 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+Gera as imagens do README (docs/images/) a partir de um acervo FICTÍCIO: nada do computador de quem
+roda aparece nas capturas. O acervo é gravado direto no índice SQLite (sem criar arquivos em disco),
+por isso a pasta "D:\\Acervo_Geoespacial" não precisa existir.
+
+Só Windows. Requer Pillow (apenas para este script: `pip install pillow`) e o Microsoft Edge (para a
+captura do treemap em HTML). Uso: python tools/screenshots.py
+"""
+import contextlib
+import io
+import json
+import os
+import random
+import shutil
+import subprocess
+import sys
+import tempfile
+import time
+
+ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+sys.path.insert(0, ROOT)
+import reperio as g # noqa: E402
+from PIL import Image, ImageDraw, ImageFont, ImageGrab # noqa: E402
+
+OUT = os.path.join(ROOT, "docs", "images")
+DEMO_ROOT = "D:\\Acervo_Geoespacial"
+NOW = time.mktime((2026, 10, 17, 9, 30, 0, 0, 0, -1))
+MB, GB = 1 << 20, 1 << 30
+
+
+def build_demo(index):
+ """Grava um acervo geoespacial fictício (≈ 120 GB, ≈ 45 mil arquivos) no índice."""
+ rnd = random.Random(7)
+ tree = {} # pasta -> (subpastas, arquivos)
+
+ def put(path, subdirs=(), files=()):
+ tree[path] = (list(subdirs), list(files))
+
+ def mt():
+ return NOW - rnd.randint(1, 900) * 86400
+
+ cenas = ("226_068", "227_069", "228_070")
+ anos = (2019, 2020, 2021, 2022, 2023)
+ p_land = DEMO_ROOT + "\\LANDSAT"
+ put(p_land, [str(a) for a in anos])
+ for a in anos:
+ pa = "%s\\%d" % (p_land, a)
+ put(pa, cenas)
+ for c in cenas:
+ base = "LC08_L2SP_%s_%d0715" % (c.replace("_", ""), a)
+ files = [("%s_%s.TIF" % (base, b), int(rnd.uniform(60, 130) * MB), mt())
+ for b in ("B2", "B3", "B4", "B5", "B6", "B7")]
+ files.append((base + "_MTL.xml", 9 * 1024, mt()))
+ put("%s\\%s" % (pa, c), (), files)
+ p_s2 = DEMO_ROOT + "\\SENTINEL"
+ muns = ("Cuiaba", "Sinop", "Rondonopolis", "Barra_do_Garcas", "Caceres", "Lucas_do_Rio_Verde")
+ put(p_s2, ["2023"])
+ put(p_s2 + "\\2023", muns)
+ for m in muns:
+ put("%s\\2023\\%s" % (p_s2, m), (), [
+ ("S2A_MSIL2A_20230812_%s_NDVI.tif" % m, int(rnd.uniform(380, 520) * MB), mt()),
+ ("S2A_MSIL2A_20230812_%s_RGB.tif" % m, int(rnd.uniform(600, 800) * MB), mt()),
+ ("S2A_MSIL2A_20230812_%s_meta.json" % m, 4096, mt())])
+ p_mos = DEMO_ROOT + "\\MOSAICOS"
+ put(p_mos, (), [("IMAGENS_mosaico_2020.tif", int(6.2 * GB), mt()),
+ ("IMAGENS_mosaico_2021.tif", int(7.1 * GB), mt()),
+ ("IMAGENS_mosaico_2022.ecw", int(1.1 * GB), mt()),
+ ("IMAGENS_mosaico_2023.tif", int(7.8 * GB), mt())])
+ p_vet = DEMO_ROOT + "\\VETORES"
+ put(p_vet, ["limites", "car"])
+ put(p_vet + "\\limites", (), [("%s%s" % (n, e), int(rnd.uniform(5, 800) * 1024), mt())
+ for n in ("Cuiaba", "Sinop", "Rondonopolis", "Caceres")
+ for e in (".shp", ".shx", ".dbf", ".prj")])
+ put(p_vet + "\\car", (), [("CAR_MT_2023.gpkg", int(1.4 * GB), mt()),
+ ("CAR_MT_2022.gpkg", int(1.2 * GB), mt())])
+ p_vid = DEMO_ROOT + "\\VIDEOS_DRONE"
+ put(p_vid, ["2022", "2023"])
+ for ano in ("2022", "2023"):
+ put("%s\\%s" % (p_vid, ano), (), [("DJI_%04d.MP4" % i, int(rnd.uniform(0.9, 3.2) * GB), mt())
+ for i in range(1, 15)])
+ p_bk = DEMO_ROOT + "\\BACKUP"
+ put(p_bk, (), [("acervo_2022_completo.zip", int(9.4 * GB), mt()),
+ ("acervo_2023_completo.zip", int(12.6 * GB), mt()),
+ ("vetores_2024-01.zip", int(0.6 * GB), mt())])
+ p_doc = DEMO_ROOT + "\\DOCUMENTOS"
+ put(p_doc, (), [("Relatorio_IMAGENS_%d_%02d.pdf" % (2020 + i // 12, i % 12 + 1),
+ int(rnd.uniform(0.2, 4.5) * MB), mt()) for i in range(60)])
+ p_til = DEMO_ROOT + "\\TILES_WEB"
+ put(p_til, ["z14", "z15"])
+ for z, n in (("z14", 9000), ("z15", 33000)):
+ put("%s\\%s" % (p_til, z), (), [("tile_%06d.png" % i, int(rnd.uniform(8, 38) * 1024), mt())
+ for i in range(n)])
+ put(DEMO_ROOT, ["LANDSAT", "SENTINEL", "MOSAICOS", "VETORES", "VIDEOS_DRONE", "BACKUP",
+ "DOCUMENTOS", "TILES_WEB"], [("LEIAME.txt", 2048, NOW)])
+ for path, (subs, files) in tree.items():
+ index.record_dir(path, subs, files, dmtime=NOW)
+ index.flush()
+
+
+def pump(root, sec):
+ end = time.time() + sec
+ while time.time() < end:
+ root.update()
+ time.sleep(0.02)
+
+
+def grab(win, name):
+ win.update()
+ x, y = win.winfo_rootx(), win.winfo_rooty()
+ ImageGrab.grab(bbox=(x, y, x + win.winfo_width(), y + win.winfo_height())).save(
+ os.path.join(OUT, name))
+ print(" ->", name)
+
+
+def block(dlg, name, depth=2, nth=0):
+ cand = [r for r in dlg._tm_rects if r["node"]["name"] == name and r["depth"] >= depth - 1]
+ return cand[nth] if cand else None
+
+
+class Ev(object):
+ def __init__(self, r):
+ self.x, self.y = int(r["x"] + r["w"] / 2), int(r["y"] + r["h"] / 2)
+
+
+def gui_shots(index_path):
+ index = g.Index(index_path)
+ root = g.tk.Tk()
+ root.geometry("1100x820+10+10")
+ app = g.App(root, index)
+ pump(root, 1.0)
+ root.lift()
+ root.attributes("-topmost", True)
+ app.v_root.set(DEMO_ROOT)
+ app.v_pat.set("*.tif")
+ app._start()
+ deadline = time.time() + 20
+ while (app._running or app._rcount == 0) and time.time() < deadline:
+ pump(root, 0.1)
+ pump(root, 0.8)
+ app.tree.selection_set(app.tree.get_children()[:3])
+ pump(root, 0.3)
+ print("janela principal")
+ grab(root, "janela_principal.png")
+
+ app._open_space()
+ dlg = app._space_win
+ dlg.win.geometry("1200x780+30+20")
+ dlg.win.attributes("-topmost", True)
+ pump(root, 2.5)
+ print("treemap por tamanho")
+ mosaicos = block(dlg, "MOSAICOS", 1)
+ dlg._tm_click(Ev(mosaicos))
+ other = block(dlg, "acervo_2023_completo.zip") or block(dlg, "BACKUP", 1)
+ dlg._tm_motion(Ev(other or mosaicos))
+ pump(root, 0.5)
+ grab(dlg.win, "espaco_treemap_tamanho.png")
+
+ print("treemap por quantidade")
+ dlg.v_metric.set("quantidade")
+ dlg._set_metric()
+ pump(root, 0.8)
+ tiles = block(dlg, "TILES_WEB", 1)
+ dlg._tm_click(Ev(tiles))
+ dlg._tm_motion(Ev(tiles))
+ pump(root, 0.5)
+ grab(dlg.win, "espaco_treemap_quantidade.png")
+
+ print("tamanho mínimo da janela de espaço")
+ dlg.v_metric.set("tamanho")
+ dlg._set_metric()
+ mw, mh = dlg.win.minsize()
+ dlg.win.geometry("%dx%d+30+20" % (mw, mh))
+ pump(root, 1.0)
+ grab(dlg.win, "espaco_tamanho_minimo.png")
+ dlg._close()
+ mw, mh = root.minsize()
+ root.geometry("%dx%d+10+10" % (mw, mh))
+ pump(root, 1.0)
+ print("tamanho mínimo da janela principal")
+ grab(root, "janela_principal_minima.png")
+ index.close()
+ root.destroy()
+
+
+def cli(*args):
+ out, err = io.StringIO(), io.StringIO()
+ with contextlib.redirect_stdout(out), contextlib.redirect_stderr(err):
+ try:
+ g.main(list(args))
+ except SystemExit:
+ pass
+ return out.getvalue()
+
+
+def terminal_png(title, blocks, dest, width=1040):
+ """Desenha uma 'janela de terminal' com os comandos e suas saídas (fonte monoespaçada)."""
+ font = None
+ for f in ("consola.ttf", "Consolas.ttf", "cour.ttf"):
+ try:
+ font = ImageFont.truetype(f, 15)
+ break
+ except OSError:
+ continue
+ font = font or ImageFont.load_default()
+ lines = []
+ for cmd, out in blocks:
+ lines.append(("$ " + cmd, "#7ee787"))
+ for ln in out.rstrip("\n").split("\n"):
+ lines.append((ln, "#d6dde4"))
+ lines.append(("", "#d6dde4"))
+ lh = 20
+ img = Image.new("RGB", (width, 36 + lh * len(lines) + 16), "#0d1117")
+ d = ImageDraw.Draw(img)
+ d.rectangle((0, 0, width, 34), fill="#1f2630")
+ for i, c in enumerate(("#ff5f56", "#ffbd2e", "#27c93f")):
+ d.ellipse((14 + i * 22, 11, 26 + i * 22, 23), fill=c)
+ d.text((100, 9), title, fill="#8b949e", font=font)
+ y = 46
+ for text, color in lines:
+ d.text((16, y), text, fill=color, font=font)
+ y += lh
+ img.save(dest)
+ print(" ->", os.path.basename(dest))
+
+
+def cli_shots(index_path):
+ base = ["--indice", index_path, "--so-indice"]
+ top = cli(*base, "--espaco", DEMO_ROOT, "--top", "6")
+ ext = cli(*base, "--extensoes", "5", "--em", DEMO_ROOT)
+ big = cli(*base, "--maiores", "5", "--em", DEMO_ROOT)
+ terminal_png("ReperIO — espaço, extensões e maiores arquivos", [
+ ("reperio --espaco D:\\Acervo_Geoespacial --top 6", top),
+ ("reperio --extensoes 5 --em D:\\Acervo_Geoespacial", ext),
+ ("reperio --maiores 5 --em D:\\Acervo_Geoespacial", big)],
+ os.path.join(OUT, "cli_texto.png"), width=1180)
+ raw = cli(*base, "--espaco", DEMO_ROOT, "--json", "--top", "2", "--profundidade", "2")
+ data = json.loads(raw)
+ for it in data["espaco"]["itens"]:
+ for ch in it.get("filhos", []):
+ ch.pop("filhos", None)
+ data.pop("varredura", None)
+ data["espaco"]["itens"] = data["espaco"]["itens"][:2]
+ for it in data["espaco"]["itens"]:
+ it["filhos"] = it.get("filhos", [])[:2]
+ for k in ("bytes_diretos", "arquivos_diretos", "subpastas", "pct_pai"):
+ it.pop(k, None)
+ for ch in it["filhos"]:
+ ch.pop(k, None)
+ data.pop("desempenho", None)
+ pretty = json.dumps(data, ensure_ascii=False, indent=2)
+ terminal_png("ReperIO --json — saída para scripts e IAs", [
+ ("reperio --espaco D:\\Acervo_Geoespacial --json --top 2 --profundidade 2 --so-indice",
+ pretty)], os.path.join(OUT, "cli_json.png"), width=1040)
+
+
+def html_shot(index_path, tmp):
+ html = os.path.join(tmp, "treemap.html")
+ cli("--indice", index_path, "--so-indice", "--espaco", DEMO_ROOT, "--treemap", html, "-q")
+ edge = shutil.which("msedge") or os.path.join(
+ os.environ.get("ProgramFiles(x86)", r"C:\Program Files (x86)"),
+ "Microsoft", "Edge", "Application", "msedge.exe")
+ if not os.path.exists(edge):
+ print(" (Edge não encontrado: pulei treemap_html.png)")
+ return
+ shot = os.path.join(OUT, "treemap_html.png")
+ subprocess.run([edge, "--headless=new", "--disable-gpu", "--hide-scrollbars",
+ "--window-size=1280,1180", "--screenshot=" + shot,
+ "file:///" + html.replace("\\", "/")], stdout=subprocess.DEVNULL,
+ stderr=subprocess.DEVNULL, timeout=90)
+ time.sleep(1)
+ print(" -> treemap_html.png")
+
+
+def main():
+ os.makedirs(OUT, exist_ok=True)
+ tmp = tempfile.mkdtemp(prefix="reperio_shots_")
+ index_path = os.path.join(tmp, "demo.sqlite")
+ try:
+ idx = g.Index(index_path)
+ build_demo(idx)
+ idx.close()
+ print("acervo fictício gravado")
+ gui_shots(index_path)
+ cli_shots(index_path)
+ html_shot(index_path, tmp)
+ finally:
+ shutil.rmtree(tmp, ignore_errors=True)
+ print("imagens em", OUT)
+
+
+if __name__ == "__main__":
+ main()