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.monitor_tab = self.tabs.add("Monitoraggio") self.backup_tab = self.tabs.add("Backup") self.queue_tab = self.tabs.add("Coda backup") self._build_monitor_tab() self._build_backup_tab() self._build_queue_tab() self._load_values() self._refresh_queue() def _build_monitor_tab(self) -> None: self.monitor_tab.grid_columnconfigure(0, weight=1) self.monitor_tab.grid_columnconfigure(1, weight=1) self.monitor_tab.grid_rowconfigure(0, weight=1) left = ctk.CTkFrame(self.monitor_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.monitor_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) actions = ctk.CTkFrame(self.monitor_tab, fg_color="transparent") actions.grid(row=1, 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_backup_tab(self) -> None: self.backup_tab.grid_columnconfigure(0, weight=1) self.backup_tab.grid_rowconfigure(1, weight=1) backup_frame = ctk.CTkFrame(self.backup_tab) backup_frame.grid(row=0, column=0, sticky="ew", padx=8, pady=8) backup_frame.grid_columnconfigure(1, weight=1) self.server_host_var = tk.StringVar() self.server_port_var = tk.StringVar() self.engine_var = tk.StringVar() self.robocopy_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, "Motore backup", self.engine_var, 2) _label_entry(backup_frame, "Percorso robocopy", self.robocopy_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) destinations_frame = ctk.CTkFrame(self.backup_tab) destinations_frame.grid(row=1, column=0, sticky="nsew", padx=8, pady=(0, 8)) destinations_frame.grid_columnconfigure(0, weight=1) destinations_frame.grid_rowconfigure(0, weight=1) self.remote_destinations = _ListEditor(destinations_frame, "Share destinazione", browse_dirs=True) self.remote_destinations.grid(row=0, column=0, sticky="nsew", padx=12, pady=12) actions = ctk.CTkFrame(self.backup_tab, fg_color="transparent") actions.grid(row=2, column=0, 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.engine_var.set(str(backup.get("engine", "robocopy"))) self.robocopy_path_var.set(str(backup.get("robocopy_path", "robocopy"))) self.remote_destinations.set_items(_configured_destinations(backup)) 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["engine"] = self.engine_var.get().strip() or "robocopy" backup["robocopy_path"] = self.robocopy_path_var.get().strip() or "robocopy" backup["remote_destinations"] = self.remote_destinations.items() backup["remote_destination"] = backup["remote_destinations"][0] if backup["remote_destinations"] else "" 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 _configured_destinations(backup: dict[str, object]) -> list[str]: destinations = backup.get("remote_destinations") if isinstance(destinations, list): return [str(item) for item in destinations] destination = str(backup.get("remote_destination", "")) return [destination] if destination else [] def main() -> int: app = BakRestConfigApp() app.mainloop() return 0 if __name__ == "__main__": raise SystemExit(main())