import customtkinter as ctk import psutil import subprocess import shutil import threading import json import os class SevenDiskApp(ctk.CTk): def __init__(self): super().__init__() # Configuration de la fenêtre principale self.title("7Disk - Moniteur de Stockage Pro") self.geometry("900x600") self.minsize(800, 500) ctk.set_appearance_mode("System") ctk.set_default_color_theme("blue") # Configuration de la grille (Sidebar + Contenu principal) self.grid_rowconfigure(0, weight=1) self.grid_columnconfigure(1, weight=1) # --- BARRE LATÉRALE (SIDEBAR) --- self.sidebar_frame = ctk.CTkFrame(self, width=200, corner_radius=0) self.sidebar_frame.grid(row=0, column=0, sticky="nsew") self.sidebar_frame.grid_rowconfigure(4, weight=1) self.logo_label = ctk.CTkLabel(self.sidebar_frame, text="7Disk ⚡", font=ctk.CTkFont(size=24, weight="bold")) self.logo_label.grid(row=0, column=0, padx=20, pady=(20, 30)) self.btn_tab_overview = ctk.CTkButton(self.sidebar_frame, text="📊 Vue d'ensemble", fg_color="transparent", text_color=("gray10", "gray90"), hover_color=("gray70", "gray30"), anchor="w", command=lambda: self.select_tab("overview")) self.btn_tab_overview.grid(row=1, column=0, padx=20, pady=10, sticky="ew") self.btn_tab_smart = ctk.CTkButton(self.sidebar_frame, text="🩺 Santé S.M.A.R.T.", fg_color="transparent", text_color=("gray10", "gray90"), hover_color=("gray70", "gray30"), anchor="w", command=lambda: self.select_tab("smart")) self.btn_tab_smart.grid(row=2, column=0, padx=20, pady=10, sticky="ew") self.appearance_mode_label = ctk.CTkLabel(self.sidebar_frame, text="Thème:", anchor="w") self.appearance_mode_label.grid(row=5, column=0, padx=20, pady=(10, 0)) self.appearance_mode_menu = ctk.CTkOptionMenu(self.sidebar_frame, values=["System", "Dark", "Light"], command=self.change_appearance_mode) self.appearance_mode_menu.grid(row=6, column=0, padx=20, pady=(10, 20)) # --- ZONE DE CONTENU PRINCIPAL --- self.main_frame = ctk.CTkFrame(self, corner_radius=10, fg_color="transparent") self.main_frame.grid(row=0, column=1, padx=20, pady=20, sticky="nsew") self.main_frame.grid_rowconfigure(1, weight=1) self.main_frame.grid_columnconfigure(0, weight=1) # Création des différentes vues self.create_overview_tab() self.create_smart_tab() # Sélection de l'onglet par défaut self.select_tab("overview") def change_appearance_mode(self, new_appearance_mode: str): ctk.set_appearance_mode(new_appearance_mode) def select_tab(self, tab_name): # Cache toutes les frames self.frame_overview.grid_forget() self.frame_smart.grid_forget() # Réinitialise les couleurs des boutons self.btn_tab_overview.configure(fg_color="transparent") self.btn_tab_smart.configure(fg_color="transparent") # Affiche la frame demandée if tab_name == "overview": self.frame_overview.grid(row=0, column=0, sticky="nsew") self.btn_tab_overview.configure(fg_color=("gray75", "gray25")) self.load_overview_data() elif tab_name == "smart": self.frame_smart.grid(row=0, column=0, sticky="nsew") self.btn_tab_smart.configure(fg_color=("gray75", "gray25")) # ========================================== # ONGLET 1 : VUE D'ENSEMBLE (Partitions) # ========================================== def create_overview_tab(self): self.frame_overview = ctk.CTkScrollableFrame(self.main_frame, fg_color="transparent") self.frame_overview.grid_columnconfigure(0, weight=1) self.lbl_overview_title = ctk.CTkLabel(self.frame_overview, text="Stockage Local", font=ctk.CTkFont(size=20, weight="bold")) self.lbl_overview_title.grid(row=0, column=0, sticky="w", pady=(0, 20)) self.partitions_frame = ctk.CTkFrame(self.frame_overview, fg_color="transparent") self.partitions_frame.grid(row=1, column=0, sticky="nsew") self.partitions_frame.grid_columnconfigure(0, weight=1) def load_overview_data(self): # Nettoyage de l'affichage existant for widget in self.partitions_frame.winfo_children(): widget.destroy() partitions = psutil.disk_partitions(all=False) row_idx = 0 for p in partitions: # Ignorer les systèmes de fichiers virtuels et les snaps de Linux if p.fstype == "" or "snap" in p.mountpoint or "boot" in p.mountpoint: continue try: usage = psutil.disk_usage(p.mountpoint) total_gb = usage.total // (2**30) used_gb = usage.used // (2**30) free_gb = usage.free // (2**30) percent = usage.percent card = ctk.CTkFrame(self.partitions_frame, corner_radius=10) card.grid(row=row_idx, column=0, sticky="ew", pady=10) card.grid_columnconfigure(1, weight=1) lbl_mount = ctk.CTkLabel(card, text=f"Point de montage : {p.mountpoint} ({p.fstype})", font=ctk.CTkFont(weight="bold")) lbl_mount.grid(row=0, column=0, columnspan=2, sticky="w", padx=15, pady=(10, 5)) lbl_info = ctk.CTkLabel(card, text=f"{used_gb} Go utilisés sur {total_gb} Go | Libre: {free_gb} Go") lbl_info.grid(row=1, column=0, sticky="w", padx=15, pady=0) lbl_percent = ctk.CTkLabel(card, text=f"{percent}%", font=ctk.CTkFont(weight="bold")) lbl_percent.grid(row=1, column=1, sticky="e", padx=15, pady=0) prog = ctk.CTkProgressBar(card) prog.grid(row=2, column=0, columnspan=2, sticky="ew", padx=15, pady=(5, 15)) prog.set(percent / 100) if percent > 90: prog.configure(progress_color="#e74c3c") # Rouge elif percent > 75: prog.configure(progress_color="#f39c12") # Orange else: prog.configure(progress_color="#2ecc71") # Vert row_idx += 1 except PermissionError: continue # ========================================== # ONGLET 2 : SANTÉ S.M.A.R.T (Physique) # ========================================== def create_smart_tab(self): self.frame_smart = ctk.CTkFrame(self.main_frame, fg_color="transparent") self.frame_smart.grid_columnconfigure(0, weight=1) self.frame_smart.grid_rowconfigure(2, weight=1) self.lbl_smart_title = ctk.CTkLabel(self.frame_smart, text="Diagnostic Matériel (S.M.A.R.T.)", font=ctk.CTkFont(size=20, weight="bold")) self.lbl_smart_title.grid(row=0, column=0, sticky="w", pady=(0, 10)) # Contrôles du haut self.controls_frame = ctk.CTkFrame(self.frame_smart, fg_color="transparent") self.controls_frame.grid(row=1, column=0, sticky="ew", pady=10) self.disk_selector = ctk.CTkOptionMenu(self.controls_frame, values=["/dev/nvme0n1", "/dev/sda", "/dev/sdb"]) self.disk_selector.pack(side="left", padx=(0, 10)) self.btn_scan = ctk.CTkButton(self.controls_frame, text="🔍 Lancer le Scan Profond", command=self.start_smart_scan) self.btn_scan.pack(side="left") self.lbl_status = ctk.CTkLabel(self.controls_frame, text="", text_color="gray") self.lbl_status.pack(side="left", padx=10) # Zone d'affichage des résultats self.textbox_smart = ctk.CTkTextbox(self.frame_smart, font=ctk.CTkFont(family="monospace", size=12)) self.textbox_smart.grid(row=2, column=0, sticky="nsew", pady=10) self.textbox_smart.insert("0.0", "Sélectionnez un disque et lancez le scan.\nLes droits administrateurs vous seront demandés via une fenêtre Zorin OS.") self.textbox_smart.configure(state="disabled") def start_smart_scan(self): disk = self.disk_selector.get() self.btn_scan.configure(state="disabled") self.lbl_status.configure(text="Analyse en cours, veuillez patienter...") self.textbox_smart.configure(state="normal") self.textbox_smart.delete("0.0", "end") self.textbox_smart.configure(state="disabled") # Lancement de l'analyse dans un Thread séparé pour ne pas figer l'interface threading.Thread(target=self.fetch_smart_data, args=(disk,), daemon=True).start() def fetch_smart_data(self, disk_path): try: # Utilisation de -j pour récupérer du JSON, beaucoup plus propre à traiter result = subprocess.run(['pkexec', 'smartctl', '-a', '-j', disk_path], capture_output=True, text=True) if result.returncode == 126 or result.returncode == 127: output_text = "❌ Erreur : Droits administrateur refusés ou smartctl non trouvé." else: try: data = json.loads(result.stdout) model = data.get("model_name", "Inconnu") serial = data.get("serial_number", "Inconnu") passed = data.get("smart_status", {}).get("passed", False) temp = data.get("temperature", {}).get("current", "N/A") status_icon = "✅ BON" if passed else "❌ DÉFAILLANT" output_text = f"=== RAPPPORT 7DISK : {disk_path} ===\n\n" output_text += f"Modèle : {model}\n" output_text += f"N° Série : {serial}\n" output_text += f"Statut S.M.A.R.T: {status_icon}\n" output_text += f"Température : {temp}°C\n\n" output_text += "--- Détails Techniques Bruts ---\n" # Ajout des informations brutes formatées if "nvme_smart_health_information_log" in data: for key, val in data["nvme_smart_health_information_log"].items(): output_text += f"{key}: {val}\n" elif "ata_smart_attributes" in data: output_text += "ID Attribut Valeur\n" for attr in data["ata_smart_attributes"].get("table", []): output_text += f"{attr['id']:<4} {attr['name']:<30} {attr['raw']['value']}\n" except json.JSONDecodeError: output_text = f"⚠️ Résultat brut (Impossible de parser le JSON):\n\n{result.stdout}" except Exception as e: output_text = f"❌ Erreur inattendue : {e}" # Mise à jour de l'interface graphique (doit être fait via mainloop, d'où self.after) self.after(0, self.update_smart_ui, output_text) def update_smart_ui(self, text): self.textbox_smart.configure(state="normal") self.textbox_smart.insert("0.0", text) self.textbox_smart.configure(state="disabled") self.lbl_status.configure(text="Analyse terminée.") self.btn_scan.configure(state="normal") if __name__ == "__main__": app = SevenDiskApp() app.mainloop()