Aggiunge GUI di configurazione

This commit is contained in:
2026-07-01 08:59:06 +02:00
parent 8f624f5b97
commit 118f01f81a
8 changed files with 418 additions and 2 deletions

343
src/bakrest/config_app.py Normal file
View File

@@ -0,0 +1,343 @@
from __future__ import annotations
import tkinter as tk
from pathlib import Path
from tkinter import filedialog, messagebox
import customtkinter as ctk
from .config import load_config, load_config_data, save_config_data
from .paths import default_config_path
from .registry import ChangeEvent, ChangeRegistry
class BakRestConfigApp(ctk.CTk):
def __init__(self) -> None:
super().__init__()
ctk.set_appearance_mode("system")
ctk.set_default_color_theme("blue")
self.title("Bak&Rest Config")
self.geometry("980x680")
self.minsize(860, 560)
self.config_path = default_config_path()
self.data = load_config_data(self.config_path)
self.app_config = load_config(self.config_path)
self.registry = ChangeRegistry(self.app_config.storage.change_registry)
self.pending_events: list[ChangeEvent] = []
self.grid_columnconfigure(0, weight=1)
self.grid_rowconfigure(1, weight=1)
self.header = ctk.CTkFrame(self, corner_radius=0)
self.header.grid(row=0, column=0, sticky="ew")
self.header.grid_columnconfigure(0, weight=1)
self.path_label = ctk.CTkLabel(
self.header,
text=f"Configurazione: {self.config_path}",
anchor="w",
)
self.path_label.grid(row=0, column=0, sticky="ew", padx=16, pady=10)
self.tabs = ctk.CTkTabview(self)
self.tabs.grid(row=1, column=0, sticky="nsew", padx=16, pady=(8, 16))
self.general_tab = self.tabs.add("Configurazione")
self.queue_tab = self.tabs.add("Coda backup")
self._build_general_tab()
self._build_queue_tab()
self._load_values()
self._refresh_queue()
def _build_general_tab(self) -> None:
self.general_tab.grid_columnconfigure(0, weight=1)
self.general_tab.grid_columnconfigure(1, weight=1)
self.general_tab.grid_rowconfigure(0, weight=1)
left = ctk.CTkFrame(self.general_tab)
left.grid(row=0, column=0, sticky="nsew", padx=(0, 8), pady=8)
left.grid_columnconfigure(0, weight=1)
left.grid_rowconfigure(1, weight=1)
left.grid_rowconfigure(3, weight=1)
right = ctk.CTkFrame(self.general_tab)
right.grid(row=0, column=1, sticky="nsew", padx=(8, 0), pady=8)
right.grid_columnconfigure(0, weight=1)
right.grid_rowconfigure(1, weight=1)
right.grid_rowconfigure(3, weight=1)
self.include_dirs = _ListEditor(left, "Cartelle monitorate", browse_dirs=True)
self.include_dirs.grid(row=0, column=0, rowspan=2, sticky="nsew", padx=12, pady=12)
self.extensions = _ListEditor(right, "Estensioni incluse")
self.extensions.grid(row=0, column=0, rowspan=2, sticky="nsew", padx=12, pady=12)
self.exclude_dirs = _ListEditor(left, "Cartelle escluse", browse_dirs=True)
self.exclude_dirs.grid(row=2, column=0, rowspan=2, sticky="nsew", padx=12, pady=12)
self.exclude_patterns = _ListEditor(right, "Pattern esclusi")
self.exclude_patterns.grid(row=2, column=0, rowspan=2, sticky="nsew", padx=12, pady=12)
backup_frame = ctk.CTkFrame(self.general_tab)
backup_frame.grid(row=1, column=0, columnspan=2, sticky="ew", padx=0, pady=(0, 8))
backup_frame.grid_columnconfigure(1, weight=1)
self.server_host_var = tk.StringVar()
self.server_port_var = tk.StringVar()
self.remote_destination_var = tk.StringVar()
self.rsync_path_var = tk.StringVar()
self.shutdown_on_success_var = tk.BooleanVar()
_label_entry(backup_frame, "Server", self.server_host_var, 0)
_label_entry(backup_frame, "Porta TCP", self.server_port_var, 1)
_label_entry(backup_frame, "Destinazione remota", self.remote_destination_var, 2)
_label_entry(backup_frame, "Percorso rsync", self.rsync_path_var, 3)
shutdown_check = ctk.CTkCheckBox(
backup_frame,
text="Spegni il PC se il backup termina correttamente",
variable=self.shutdown_on_success_var,
)
shutdown_check.grid(row=4, column=0, columnspan=2, sticky="w", padx=12, pady=8)
actions = ctk.CTkFrame(self.general_tab, fg_color="transparent")
actions.grid(row=2, column=0, columnspan=2, sticky="ew", pady=(0, 8))
actions.grid_columnconfigure(0, weight=1)
save_button = ctk.CTkButton(actions, text="Salva configurazione", command=self._save_config)
save_button.grid(row=0, column=1, padx=8, pady=4)
reload_button = ctk.CTkButton(actions, text="Ricarica", command=self._reload_config)
reload_button.grid(row=0, column=2, padx=8, pady=4)
def _build_queue_tab(self) -> None:
self.queue_tab.grid_columnconfigure(0, weight=1)
self.queue_tab.grid_rowconfigure(1, weight=1)
toolbar = ctk.CTkFrame(self.queue_tab, fg_color="transparent")
toolbar.grid(row=0, column=0, sticky="ew", padx=8, pady=8)
toolbar.grid_columnconfigure(0, weight=1)
self.queue_count_label = ctk.CTkLabel(toolbar, text="File in coda: 0", anchor="w")
self.queue_count_label.grid(row=0, column=0, sticky="ew", padx=4)
refresh_button = ctk.CTkButton(toolbar, text="Aggiorna", command=self._refresh_queue)
refresh_button.grid(row=0, column=1, padx=4)
remove_button = ctk.CTkButton(toolbar, text="Rimuovi selezionato", command=self._remove_selected_pending)
remove_button.grid(row=0, column=2, padx=4)
list_frame = ctk.CTkFrame(self.queue_tab)
list_frame.grid(row=1, column=0, sticky="nsew", padx=8, pady=(0, 8))
list_frame.grid_columnconfigure(0, weight=1)
list_frame.grid_rowconfigure(0, weight=1)
self.queue_list = tk.Listbox(list_frame, activestyle="dotbox", exportselection=False)
self.queue_list.grid(row=0, column=0, sticky="nsew", padx=(8, 0), pady=8)
scrollbar = ctk.CTkScrollbar(list_frame, command=self.queue_list.yview)
scrollbar.grid(row=0, column=1, sticky="ns", padx=(4, 8), pady=8)
self.queue_list.configure(yscrollcommand=scrollbar.set)
def _load_values(self) -> None:
watch = self.data.setdefault("watch", {})
backup = self.data.setdefault("backup", {})
server_check = backup.setdefault("server_check", {})
self.include_dirs.set_items(watch.get("include_dirs", []))
self.extensions.set_items(watch.get("include_extensions", []))
self.exclude_dirs.set_items(watch.get("exclude_dirs", []))
self.exclude_patterns.set_items(watch.get("exclude_patterns", []))
self.server_host_var.set(str(backup.get("server_host", "")))
self.server_port_var.set(str(server_check.get("port", 22)))
self.remote_destination_var.set(str(backup.get("remote_destination", "")))
self.rsync_path_var.set(str(backup.get("rsync_path", "rsync")))
self.shutdown_on_success_var.set(bool(backup.get("shutdown_on_success", True)))
def _save_config(self) -> None:
watch = self.data.setdefault("watch", {})
backup = self.data.setdefault("backup", {})
server_check = backup.setdefault("server_check", {})
watch["include_dirs"] = self.include_dirs.items()
watch["include_extensions"] = self.extensions.items()
watch["exclude_dirs"] = self.exclude_dirs.items()
watch["exclude_patterns"] = self.exclude_patterns.items()
backup["server_host"] = self.server_host_var.get().strip()
backup["remote_destination"] = self.remote_destination_var.get().strip()
backup["rsync_path"] = self.rsync_path_var.get().strip() or "rsync"
backup["shutdown_on_success"] = bool(self.shutdown_on_success_var.get())
server_check["port"] = _safe_int(self.server_port_var.get(), 22)
save_config_data(self.data, self.config_path)
self.app_config = load_config(self.config_path)
self.registry = ChangeRegistry(self.app_config.storage.change_registry)
messagebox.showinfo("Bak&Rest", "Configurazione salvata.")
def _reload_config(self) -> None:
self.data = load_config_data(self.config_path)
self.app_config = load_config(self.config_path)
self.registry = ChangeRegistry(self.app_config.storage.change_registry)
self._load_values()
self._refresh_queue()
def _refresh_queue(self) -> None:
self.pending_events = self.registry.list_pending()
self.queue_list.delete(0, tk.END)
for event in self.pending_events:
self.queue_list.insert(tk.END, f"{event.event_type} | {event.path}")
self.queue_count_label.configure(text=f"File in coda: {len(self.pending_events)}")
def _remove_selected_pending(self) -> None:
selection = self.queue_list.curselection()
if not selection:
messagebox.showinfo("Bak&Rest", "Seleziona un file dalla coda.")
return
event = self.pending_events[selection[0]]
dialog = RemovePendingDialog(self, event.path)
self.wait_window(dialog)
if dialog.choice == "file":
self.registry.mark_ignored(event.path)
elif dialog.choice == "folder":
folder = str(Path(event.path).parent)
self._add_exclude_dir(folder)
self.registry.mark_status(self._pending_paths_inside(folder), "ignored")
save_config_data(self.data, self.config_path)
self.exclude_dirs.set_items(self.data.setdefault("watch", {}).get("exclude_dirs", []))
else:
return
self._refresh_queue()
def _add_exclude_dir(self, folder: str) -> None:
watch = self.data.setdefault("watch", {})
exclude_dirs = list(watch.get("exclude_dirs", []))
if folder not in exclude_dirs:
exclude_dirs.append(folder)
watch["exclude_dirs"] = exclude_dirs
def _pending_paths_inside(self, folder: str) -> list[str]:
folder_path = Path(folder)
paths: list[str] = []
for event in self.pending_events:
try:
Path(event.path).relative_to(folder_path)
paths.append(event.path)
except ValueError:
continue
return paths
class RemovePendingDialog(ctk.CTkToplevel):
def __init__(self, parent: ctk.CTk, file_path: str) -> None:
super().__init__(parent)
self.choice: str | None = None
self.title("Rimuovi dalla coda")
self.geometry("520x190")
self.resizable(False, False)
self.transient(parent)
self.grab_set()
self.grid_columnconfigure(0, weight=1)
label = ctk.CTkLabel(
self,
text="Vuoi rimuovere solo il file o isolare dal backup la cartella che lo contiene?",
wraplength=470,
justify="left",
)
label.grid(row=0, column=0, sticky="ew", padx=18, pady=(18, 8))
path_label = ctk.CTkLabel(self, text=file_path, wraplength=470, justify="left")
path_label.grid(row=1, column=0, sticky="ew", padx=18, pady=(0, 16))
buttons = ctk.CTkFrame(self, fg_color="transparent")
buttons.grid(row=2, column=0, sticky="e", padx=12, pady=12)
ctk.CTkButton(buttons, text="Solo file", command=lambda: self._close("file")).grid(row=0, column=0, padx=6)
ctk.CTkButton(buttons, text="Isola cartella", command=lambda: self._close("folder")).grid(row=0, column=1, padx=6)
ctk.CTkButton(buttons, text="Annulla", command=lambda: self._close(None)).grid(row=0, column=2, padx=6)
def _close(self, choice: str | None) -> None:
self.choice = choice
self.destroy()
class _ListEditor(ctk.CTkFrame):
def __init__(self, parent: ctk.CTkBaseClass, title: str, browse_dirs: bool = False) -> None:
super().__init__(parent)
self.browse_dirs = browse_dirs
self.grid_columnconfigure(0, weight=1)
self.grid_rowconfigure(1, weight=1)
label = ctk.CTkLabel(self, text=title, anchor="w")
label.grid(row=0, column=0, columnspan=3, sticky="ew", padx=10, pady=(8, 4))
self.listbox = tk.Listbox(self, activestyle="dotbox", exportselection=False)
self.listbox.grid(row=1, column=0, columnspan=3, sticky="nsew", padx=10, pady=(0, 8))
self.entry_var = tk.StringVar()
entry = ctk.CTkEntry(self, textvariable=self.entry_var)
entry.grid(row=2, column=0, sticky="ew", padx=(10, 4), pady=(0, 10))
add_button = ctk.CTkButton(self, text="+", width=42, command=self._add)
add_button.grid(row=2, column=1, padx=4, pady=(0, 10))
remove_button = ctk.CTkButton(self, text="-", width=42, command=self._remove)
remove_button.grid(row=2, column=2, padx=(4, 10), pady=(0, 10))
if browse_dirs:
browse_button = ctk.CTkButton(self, text="Sfoglia", command=self._browse)
browse_button.grid(row=3, column=0, columnspan=3, sticky="ew", padx=10, pady=(0, 10))
def set_items(self, items: list[str]) -> None:
self.listbox.delete(0, tk.END)
for item in items:
self.listbox.insert(tk.END, item)
def items(self) -> list[str]:
return [str(self.listbox.get(index)) for index in range(self.listbox.size())]
def _add(self) -> None:
value = self.entry_var.get().strip()
if not value:
return
if value not in self.items():
self.listbox.insert(tk.END, value)
self.entry_var.set("")
def _remove(self) -> None:
selection = self.listbox.curselection()
if selection:
self.listbox.delete(selection[0])
def _browse(self) -> None:
directory = filedialog.askdirectory()
if directory:
self.entry_var.set(directory)
self._add()
def _label_entry(parent: ctk.CTkFrame, label: str, variable: tk.StringVar, row: int) -> None:
ctk.CTkLabel(parent, text=label, anchor="w").grid(row=row, column=0, sticky="w", padx=12, pady=6)
ctk.CTkEntry(parent, textvariable=variable).grid(row=row, column=1, sticky="ew", padx=12, pady=6)
def _safe_int(value: str, default: int) -> int:
try:
return int(value)
except ValueError:
return default
def main() -> int:
app = BakRestConfigApp()
app.mainloop()
return 0
if __name__ == "__main__":
raise SystemExit(main())