#!/usr/bin/env python3 import sys, time, subprocess import psutil from collections import deque from PyQt6.QtCore import Qt, QTimer, QThread, pyqtSignal, QPointF, QRectF, QPropertyAnimation, QEasingCurve, pyqtProperty from PyQt6.QtGui import QFont, QPainter, QColor, QPen, QBrush, QPainterPath, QAction, QIcon from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QProgressBar, QSystemTrayIcon, QMenu, QGridLayout, QStackedWidget, QPushButton, QFrame, QScrollArea, QSlider) # ========================================== # THREAD DE DONNÉES (CPU, RAM, GPU, RESEAU) # ========================================== class UltimateMonitorThread(QThread): stats_updated = pyqtSignal(dict) def __init__(self): super().__init__() self.interval = 1000 self.net_io = psutil.net_io_counters() self.last_time = time.time() def run(self): while True: current_time = time.time() dt = current_time - self.last_time new_net_io = psutil.net_io_counters() stats = { 'cpu_temp': self.get_cpu_temp(), 'cpu_percent': psutil.cpu_percent(interval=None), 'cores': self.get_cores_temp(), 'ram': psutil.virtual_memory().percent, 'swap': psutil.swap_memory().percent, 'disk': psutil.disk_usage('/').percent, 'net_dl': (new_net_io.bytes_recv - self.net_io.bytes_recv) / dt / 1024 / 1024, # MB/s 'net_ul': (new_net_io.bytes_sent - self.net_io.bytes_sent) / dt / 1024 / 1024, # MB/s 'gpu': self.get_gpu_info() } self.net_io = new_net_io self.last_time = current_time self.stats_updated.emit(stats) self.msleep(self.interval) def get_cpu_temp(self): temps = psutil.sensors_temperatures() for name in ['coretemp', 'acpitz', 'cpu_thermal', 'k10temp']: if name in temps: return int(temps[name][0].current) return 40 def get_cores_temp(self): cores = [] temps = psutil.sensors_temperatures() if 'coretemp' in temps: cores = [(e.label, int(e.current)) for e in temps['coretemp'] if 'Core' in e.label] return cores if cores else [(f"Core {i}", 40) for i in range(psutil.cpu_count(logical=False) or 4)] def get_gpu_info(self): # Tente de récupérer les infos NVIDIA en priorité try: res = subprocess.check_output(['nvidia-smi', '--query-gpu=temperature.gpu,utilization.gpu', '--format=csv,noheader,nounits'], encoding='utf-8') temp, util = map(int, res.strip().split(', ')) return {"name": "NVIDIA GPU", "temp": temp, "util": util} except: # Fallback sur les capteurs standards (AMD/Intel) temps = psutil.sensors_temperatures() if 'amdgpu' in temps: return {"name": "AMD GPU", "temp": int(temps['amdgpu'][0].current), "util": 0} return None # ========================================== # WIDGETS ANIMÉS ZORIN OS PRO # ========================================== class SmoothProgressBar(QProgressBar): def __init__(self, color="#1E88E5"): super().__init__() self.setFixedHeight(6) self.setTextVisible(False) self._color = color self._value = 0 self.anim = QPropertyAnimation(self, b"animated_value") self.anim.setEasingCurve(QEasingCurve.Type.OutCubic) self.anim.setDuration(400) self.update_style() def update_style(self): self.setStyleSheet(f""" QProgressBar {{ background-color: #222222; border-radius: 3px; }} QProgressBar::chunk {{ background-color: {self._color}; border-radius: 3px; }} """) def set_color(self, color): self._color = color self.update_style() @pyqtProperty(int) def animated_value(self): return self._value @animated_value.setter def animated_value(self, val): self._value = val super().setValue(val) def set_smooth_value(self, val): self.anim.setStartValue(self._value) self.anim.setEndValue(val) self.anim.start() class ModernGauge(QWidget): def __init__(self, title, color="#1E88E5"): super().__init__() self.setFixedSize(180, 180) self.value = 0 self.title = title self.color = QColor(color) def set_value(self, val): self.value = val self.update() def set_color(self, hex_color): self.color = QColor(hex_color) self.update() def paintEvent(self, event): painter = QPainter(self) painter.setRenderHint(QPainter.RenderHint.Antialiasing) rect = QRectF(20, 20, 140, 140) painter.setPen(QPen(QColor("#2A2A2A"), 12, Qt.PenStyle.SolidLine, Qt.PenCapStyle.RoundCap)) painter.drawArc(rect, 135 * 16, -270 * 16) span = int(-270 * (self.value / 100.0) * 16) painter.setPen(QPen(self.color, 12, Qt.PenStyle.SolidLine, Qt.PenCapStyle.RoundCap)) painter.drawArc(rect, 135 * 16, span) painter.setPen(QColor("#FFFFFF")) painter.setFont(QFont("Inter", 32, QFont.Weight.Thin)) painter.drawText(rect, Qt.AlignmentFlag.AlignCenter, f"{self.value}°") painter.setFont(QFont("Inter", 9, QFont.Weight.Bold)) painter.setPen(QColor("#888888")) painter.drawText(QRectF(20, 140, 140, 30), Qt.AlignmentFlag.AlignCenter, self.title) # ========================================== # INTERFACE PRINCIPALE # ========================================== class UltimateApp(QMainWindow): def __init__(self): super().__init__() self.accent_color = "#1E88E5" # Bleu Zorin par défaut self.init_ui() self.init_tray() self.thread = UltimateMonitorThread() self.thread.stats_updated.connect(self.update_data) self.thread.start() def init_ui(self): self.setWindowTitle("7Cooler Ultimate") self.resize(850, 550) self.setStyleSheet("QMainWindow { background-color: #0E0E0E; color: #FFFFFF; }") main_widget = QWidget() self.setCentralWidget(main_widget) main_layout = QHBoxLayout(main_widget) main_layout.setContentsMargins(0, 0, 0, 0) main_layout.setSpacing(0) # --- MENU LATÉRAL --- sidebar = QFrame() sidebar.setFixedWidth(200) sidebar.setStyleSheet("background-color: #151515; border-right: 1px solid #222;") side_layout = QVBoxLayout(sidebar) side_layout.setContentsMargins(10, 30, 10, 30) brand = QLabel("7COOLER") brand.setFont(QFont("Inter", 16, QFont.Weight.Black)) brand.setAlignment(Qt.AlignmentFlag.AlignCenter) brand.setStyleSheet("letter-spacing: 2px; margin-bottom: 30px;") side_layout.addWidget(brand) self.btn_dash = self.create_nav_btn("📊 Tableau de bord", 0) self.btn_hard = self.create_nav_btn("🌡️ Matériel & Capteurs", 1) self.btn_set = self.create_nav_btn("⚙️ Paramètres", 2) side_layout.addWidget(self.btn_dash) side_layout.addWidget(self.btn_hard) side_layout.addWidget(self.btn_set) side_layout.addStretch() # --- CONTENU CENTRAL (PAGES) --- self.stack = QStackedWidget() self.stack.setStyleSheet("background-color: #0E0E0E;") self.page_dash = self.build_dashboard() self.page_hard = self.build_hardware() self.page_set = self.build_settings() self.stack.addWidget(self.page_dash) self.stack.addWidget(self.page_hard) self.stack.addWidget(self.page_set) main_layout.addWidget(sidebar) main_layout.addWidget(self.stack) self.switch_page(0) def create_nav_btn(self, text, index): btn = QPushButton(text) btn.setFont(QFont("Inter", 10, QFont.Weight.Bold)) btn.setCursor(Qt.CursorShape.PointingHandCursor) btn.setStyleSheet(""" QPushButton { text-align: left; padding: 12px; border-radius: 8px; background: transparent; color: #888; border: none; } QPushButton:hover { background: #222; color: #FFF; } """) btn.clicked.connect(lambda: self.switch_page(index)) return btn def switch_page(self, index): self.stack.setCurrentIndex(index) for i, btn in enumerate([self.btn_dash, self.btn_hard, self.btn_set]): if i == index: btn.setStyleSheet(f"text-align: left; padding: 12px; border-radius: 8px; background: {self.accent_color}; color: #FFF; font-weight: bold; border: none;") else: btn.setStyleSheet("text-align: left; padding: 12px; border-radius: 8px; background: transparent; color: #888; border: none;") # --- PAGE 1: DASHBOARD --- def build_dashboard(self): page = QWidget() layout = QVBoxLayout(page) layout.setContentsMargins(40, 40, 40, 40) title = QLabel("Vue d'ensemble") title.setFont(QFont("Inter", 22, QFont.Weight.Bold)) layout.addWidget(title) # Jauges Hautes gauge_layout = QHBoxLayout() self.cpu_gauge = ModernGauge("CPU TEMP") self.gpu_gauge = ModernGauge("GPU TEMP") gauge_layout.addWidget(self.cpu_gauge) gauge_layout.addWidget(self.gpu_gauge) gauge_layout.addStretch() layout.addLayout(gauge_layout) layout.addSpacing(30) # Barres d'utilisation self.bars = {} for name, label in [('cpu', 'Charge CPU'), ('ram', 'Mémoire Vive'), ('swap', 'Fichier d\'échange'), ('disk', 'Stockage Principal')]: lbl = QLabel(f"{label}: 0%") lbl.setFont(QFont("Inter", 10)) bar = SmoothProgressBar() self.bars[name] = {'lbl': lbl, 'bar': bar} layout.addWidget(lbl) layout.addWidget(bar) layout.addSpacing(10) layout.addStretch() return page # --- PAGE 2: HARDWARE --- def build_hardware(self): page = QWidget() layout = QVBoxLayout(page) layout.setContentsMargins(40, 40, 40, 40) title = QLabel("Capteurs Détaillés") title.setFont(QFont("Inter", 22, QFont.Weight.Bold)) layout.addWidget(title) scroll = QScrollArea() scroll.setWidgetResizable(True) scroll.setStyleSheet("QScrollArea { border: none; background: transparent; }") content = QWidget() self.grid = QGridLayout(content) scroll.setWidget(content) layout.addWidget(scroll) # Réseau UI self.net_lbl = QLabel("Réseau: DL 0 MB/s | UL 0 MB/s") self.net_lbl.setFont(QFont("Inter", 12, QFont.Weight.Bold)) self.net_lbl.setStyleSheet("color: #888;") layout.addWidget(self.net_lbl) return page # --- PAGE 3: SETTINGS --- def build_settings(self): page = QWidget() layout = QVBoxLayout(page) layout.setContentsMargins(40, 40, 40, 40) title = QLabel("Paramètres de l'Application") title.setFont(QFont("Inter", 22, QFont.Weight.Bold)) layout.addWidget(title) # Thèmes theme_lbl = QLabel("Couleur d'accentuation") theme_lbl.setFont(QFont("Inter", 12, QFont.Weight.Bold)) layout.addWidget(theme_lbl) colors_layout = QHBoxLayout() for c in ["#1E88E5", "#E53935", "#43A047", "#8E24AA", "#FDD835", "#FFFFFF"]: btn = QPushButton() btn.setFixedSize(40, 40) btn.setStyleSheet(f"background-color: {c}; border-radius: 20px; border: 2px solid #333;") btn.setCursor(Qt.CursorShape.PointingHandCursor) btn.clicked.connect(lambda checked, col=c: self.change_accent(col)) colors_layout.addWidget(btn) colors_layout.addStretch() layout.addLayout(colors_layout) layout.addSpacing(30) # Intervalle speed_lbl = QLabel("Vitesse de rafraîchissement (ms)") speed_lbl.setFont(QFont("Inter", 12, QFont.Weight.Bold)) layout.addWidget(speed_lbl) self.slider = QSlider(Qt.Orientation.Horizontal) self.slider.setRange(500, 3000) self.slider.setValue(1000) self.slider.setTickPosition(QSlider.TickPosition.TicksBelow) self.slider.setTickInterval(500) self.slider.valueChanged.connect(self.change_speed) layout.addWidget(self.slider) layout.addStretch() return page def change_accent(self, color): self.accent_color = color self.cpu_gauge.set_color(color) self.gpu_gauge.set_color(color) for b in self.bars.values(): b['bar'].set_color(color) self.switch_page(self.stack.currentIndex()) # Refresh menu color def change_speed(self, val): self.thread.interval = val def update_data(self, stats): # Update Dashboard self.cpu_gauge.set_value(stats['cpu_temp']) if stats['gpu']: self.gpu_gauge.set_value(stats['gpu']['temp']) self.gpu_gauge.title = stats['gpu']['name'].upper() else: self.gpu_gauge.set_value(0) self.gpu_gauge.title = "NO GPU" self.bars['cpu']['lbl'].setText(f"Charge CPU: {stats['cpu_percent']}%") self.bars['cpu']['bar'].set_smooth_value(int(stats['cpu_percent'])) self.bars['ram']['lbl'].setText(f"Mémoire Vive: {stats['ram']}%") self.bars['ram']['bar'].set_smooth_value(int(stats['ram'])) self.bars['swap']['lbl'].setText(f"Fichier d'échange: {stats['swap']}%") self.bars['swap']['bar'].set_smooth_value(int(stats['swap'])) self.bars['disk']['lbl'].setText(f"Stockage Principal: {stats['disk']}%") self.bars['disk']['bar'].set_smooth_value(int(stats['disk'])) # Update Network self.net_lbl.setText(f"Réseau: ↓ {stats['net_dl']:.2f} MB/s | ↑ {stats['net_ul']:.2f} MB/s") # Update Hardware Grid for i in reversed(range(self.grid.count())): w = self.grid.itemAt(i).widget() if w: w.setParent(None) row, col = 0, 0 for name, temp in stats['cores']: card = QFrame() card.setStyleSheet("background-color: #1A1A1A; border-radius: 10px; padding: 10px;") cl = QVBoxLayout(card) n = QLabel(name) n.setStyleSheet("color: #888; font-size: 11px;") t = QLabel(f"{temp}°C") t.setFont(QFont("Inter", 16, QFont.Weight.Bold)) if temp > 85: t.setStyleSheet("color: #E53935;") cl.addWidget(n) cl.addWidget(t) self.grid.addWidget(card, row, col) col += 1 if col > 3: col, row = 0, row + 1 self.tray_icon.setToolTip(f"CPU: {stats['cpu_temp']}°C | RAM: {stats['ram']}%") def init_tray(self): self.tray_icon = QSystemTrayIcon(self) self.tray_icon.setIcon(QIcon.fromTheme("utilities-system-monitor")) menu = QMenu() show_action = QAction("Ouvrir 7Cooler Ultimate", self) show_action.triggered.connect(self.show) quit_action = QAction("Quitter", self) quit_action.triggered.connect(QApplication.instance().quit) menu.addAction(show_action) menu.addAction(quit_action) self.tray_icon.setContextMenu(menu) self.tray_icon.show() def closeEvent(self, event): event.ignore() self.hide() self.tray_icon.showMessage("7Cooler Ultimate", "Fonctionne en arrière-plan", QSystemTrayIcon.MessageIcon.Information, 2000) if __name__ == "__main__": app = QApplication(sys.argv) QApplication.setQuitOnLastWindowClosed(False) app.setFont(QFont("Inter", 10)) window = UltimateApp() window.show() sys.exit(app.exec())