Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions CITATION.cff
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
169 changes: 130 additions & 39 deletions README.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/CLI.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
Binary file added docs/images/cli_json.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/cli_texto.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/espaco_tamanho_minimo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/espaco_treemap_quantidade.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/espaco_treemap_tamanho.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/janela_principal.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/janela_principal_minima.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/treemap_html.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
65 changes: 52 additions & 13 deletions reperio.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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()
Expand All @@ -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:
Expand Down Expand Up @@ -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")
Expand All @@ -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")
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
77 changes: 77 additions & 0 deletions tests/test_cli_treemap.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Loading
Loading