from __future__ import annotations import argparse import json import logging import threading import time import tkinter as tk from tkinter import messagebox from pathlib import Path from PIL import Image, ImageDraw import pystray from .backup import run_backup, shutdown_after_backup from .backup_nogui import run_nogui_backup from .config import AppConfig, load_config from .logging_setup import configure_logging from .registry import ChangeRegistry class BakRestTrayApp: def __init__(self, config: AppConfig, logger: logging.Logger) -> None: self.config = config self.logger = logger self.registry = ChangeRegistry(config.storage.change_registry) self.stop_event = threading.Event() self.last_alert_reachable: bool | None = None self.icon = pystray.Icon( "BakRestTray", _create_icon_image(), "Bak&Rest", self._build_menu(), ) def run(self) -> None: self.logger.info("BakRestTray started") threading.Thread(target=self._server_status_monitor, daemon=True).start() self.icon.run() def _build_menu(self) -> pystray.Menu: return pystray.Menu( pystray.MenuItem(lambda item: self._status_text(), None, enabled=False), pystray.Menu.SEPARATOR, pystray.MenuItem("Backup e spegni", self._backup_and_shutdown), pystray.MenuItem("Aggiorna stato", self._refresh), pystray.MenuItem("Esci", self._quit), ) def _status_text(self) -> str: pending = self.registry.pending_count() server = self._read_server_status() return f"Server: {server} | File in attesa: {pending}" def _read_server_status(self) -> str: status = self._read_server_status_data() if status is None: return "sconosciuto" return "raggiungibile" if status.get("reachable") else "non raggiungibile" def _read_server_status_data(self) -> dict[str, object] | None: path = self.config.storage.status_file if not path.exists(): return None try: return json.loads(path.read_text(encoding="utf-8")) except (OSError, json.JSONDecodeError): return None def _backup_and_shutdown(self, icon: pystray.Icon, item: pystray.MenuItem) -> None: threading.Thread(target=self._backup_and_shutdown_worker, daemon=True).start() def _backup_and_shutdown_worker(self) -> None: self.logger.info("User requested Backup e spegni") self.icon.title = "Bak&Rest - backup in corso" result = run_backup(self.config, self.logger) if result.success: self.icon.title = "Bak&Rest - backup completato" shutdown_after_backup(self.config, self.logger) else: self.icon.title = "Bak&Rest - backup fallito" if result.error_code == "server_unreachable": _show_error_message("Bak&Rest", "Server non raggiungibile. E' spento?") else: _show_error_message("Bak&Rest", result.message) self.icon.update_menu() def _refresh(self, icon: pystray.Icon, item: pystray.MenuItem) -> None: self.icon.update_menu() def _quit(self, icon: pystray.Icon, item: pystray.MenuItem) -> None: self.logger.info("BakRestTray stopped") self.stop_event.set() self.icon.stop() def _server_status_monitor(self) -> None: interval = min(max(self.config.backup.server_check.interval_seconds, 60), 600) while not self.stop_event.wait(interval): status = self._read_server_status_data() if status is None: continue reachable = bool(status.get("reachable")) if reachable: self.last_alert_reachable = True continue if self.last_alert_reachable is not False: self.last_alert_reachable = False self.logger.warning("Showing backup server unreachable alert") _show_error_message( "Bak&Rest", "Accendi il server di backup o verifica che sia connesso alla rete", ) def _create_icon_image() -> Image.Image: image = Image.new("RGBA", (64, 64), (26, 38, 52, 255)) draw = ImageDraw.Draw(image) draw.rounded_rectangle((10, 14, 54, 50), radius=8, fill=(77, 163, 255, 255)) draw.rectangle((18, 24, 46, 44), fill=(245, 248, 252, 255)) draw.line((22, 34, 32, 42, 46, 26), fill=(26, 38, 52, 255), width=5) return image def _show_error_message(title: str, message: str) -> None: root = tk.Tk() root.withdraw() root.attributes("-topmost", True) try: messagebox.showerror(title, message, parent=root) finally: root.destroy() def main(argv: list[str] | None = None) -> int: parser = argparse.ArgumentParser(description="Bak&Rest tray app") parser.add_argument( "--nogui", action="store_true", help="Run backup and shutdown without tray icon", ) parser.add_argument( "--no-shutdown", action="store_true", help="With --nogui, run the backup but do not shut down the computer", ) args = parser.parse_args(argv) if args.nogui: return run_nogui_backup(shutdown=not args.no_shutdown) config = load_config() logger = configure_logging(config.storage.logs_dir, "tray") app = BakRestTrayApp(config, logger) app.run() return 0 if __name__ == "__main__": raise SystemExit(main())