Aggiunge GUI di configurazione
This commit is contained in:
343
src/bakrest/config_app.py
Normal file
343
src/bakrest/config_app.py
Normal 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())
|
||||
Reference in New Issue
Block a user