349 lines
9.6 KiB
Python
Executable File
349 lines
9.6 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
# Lancement: cpu-monitor
|
|
|
|
import tkinter as tk
|
|
from tkinter import ttk
|
|
import subprocess
|
|
import tempfile
|
|
import os
|
|
import time
|
|
|
|
import psutil
|
|
|
|
import gi
|
|
try:
|
|
gi.require_version('AyatanaAppIndicator3', '0.1')
|
|
from gi.repository import AyatanaAppIndicator3, GLib, Gtk
|
|
HAVE_INDICATOR = True
|
|
except (ImportError, ValueError):
|
|
HAVE_INDICATOR = False
|
|
|
|
|
|
MAX_POINTS = 60
|
|
UPDATE_INTERVAL = 1000 # ms
|
|
|
|
|
|
try:
|
|
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
|
|
from matplotlib.figure import Figure
|
|
HAVE_MPL = True
|
|
except ImportError:
|
|
HAVE_MPL = False
|
|
|
|
|
|
class Tracker:
|
|
def __init__(self):
|
|
self.cpu_data = []
|
|
self.ram_data = []
|
|
self.disk_data = []
|
|
self._last_cpu = None
|
|
now = time.time()
|
|
self.time_data = [now]
|
|
|
|
def _net_rate(self, curr, last, interval):
|
|
delta = curr - last
|
|
rate = max(delta / interval, 0)
|
|
return max(int(rate), 0)
|
|
|
|
def update(self, dt):
|
|
cpu = psutil.cpu_percent(interval=None)
|
|
ram = psutil.virtual_memory().percent
|
|
disk = psutil.disk_usage('/').percent
|
|
self._last_cpu = cpu
|
|
t = time.time()
|
|
self.time_data.append(t)
|
|
|
|
self.cpu_data.append(cpu)
|
|
self.ram_data.append(ram)
|
|
self.disk_data.append(disk)
|
|
|
|
net = psutil.net_io_counters()
|
|
sent = net.bytes_sent
|
|
recv = net.bytes_recv
|
|
self._last_net_sent = sent
|
|
self._last_net_recv = recv
|
|
|
|
def get_latest(self):
|
|
if self._last_cpu is None:
|
|
self.update(1)
|
|
return self._last_cpu, self.ram_data[-1] if self.ram_data else 0
|
|
|
|
def get_disk(self):
|
|
return self.disk_data[-1] if self.disk_data else 0
|
|
|
|
def get_net_rates(self):
|
|
return 0, 0
|
|
|
|
def cpu_percent(self):
|
|
return psutil.cpu_percent(interval=None)
|
|
|
|
|
|
class SysTrayIndicator:
|
|
def __init__(self, monitor):
|
|
if not HAVE_INDICATOR:
|
|
return
|
|
|
|
self.indicator = AyatanaAppIndicator3.Indicator.new(
|
|
"cpu-monitor",
|
|
"cpu-monitor",
|
|
AyatanaAppIndicator3.IndicatorCategory.SYSTEM_SERVICES
|
|
)
|
|
self.indicator.set_menu(self._create_menu())
|
|
self.monitor = monitor
|
|
self._update_icon()
|
|
|
|
try:
|
|
import cairo
|
|
except ImportError:
|
|
return
|
|
|
|
def _create_menu(self):
|
|
menu = Gtk.Menu()
|
|
|
|
item = Gtk.MenuItem(label=f"CPU: {psutil.cpu_percent(interval=None)}%")
|
|
item.show()
|
|
menu.append(item)
|
|
|
|
item2 = Gtk.MenuItem(label=f"RAM: {psutil.virtual_memory().percent}%")
|
|
item2.show()
|
|
menu.append(item2)
|
|
|
|
sep = Gtk.SeparatorMenuItem()
|
|
sep.show()
|
|
menu.append(sep)
|
|
|
|
open_item = Gtk.MenuItem(label="Open Monitor")
|
|
open_item.connect("activate", lambda w: subprocess.Popen(["cpu-monitor"]))
|
|
open_item.show()
|
|
menu.append(open_item)
|
|
|
|
close_item = Gtk.MenuItem(label="Quit")
|
|
close_item.connect("activate", lambda w: Gtk.main_quit())
|
|
close_item.show()
|
|
menu.append(close_item)
|
|
|
|
return menu
|
|
|
|
def _update_icon(self):
|
|
if not HAVE_INDICATOR:
|
|
return
|
|
try:
|
|
import cairo
|
|
except ImportError:
|
|
return
|
|
|
|
cpu = psutil.cpu_percent()
|
|
if cpu > 80:
|
|
color = (0.93, 0.33, 0.31)
|
|
elif cpu > 50:
|
|
color = (1.0, 0.55, 0.0)
|
|
else:
|
|
color = (0.40, 0.76, 0.97)
|
|
|
|
size = 24
|
|
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, size, size)
|
|
cr = cairo.Context(surface)
|
|
cr.set_source_rgba(*color, 1.0)
|
|
cr.paint()
|
|
|
|
tmp = tempfile.NamedTemporaryFile(suffix='.png', delete=False, dir='/tmp')
|
|
tmp.close()
|
|
surface.write_to_png(tmp.name)
|
|
|
|
try:
|
|
self.indicator.set_icon_full(tmp.name, "CPU Monitor")
|
|
except Exception:
|
|
pass
|
|
|
|
GLib.timeout_add_seconds(2, self._update_icon)
|
|
|
|
|
|
def create_tray_icon(monitor):
|
|
if HAVE_INDICATOR:
|
|
return SysTrayIndicator(monitor)
|
|
return None
|
|
|
|
|
|
class App:
|
|
cpu_data = []
|
|
ram_data = []
|
|
_last_cpu = None
|
|
|
|
def __init__(self, root):
|
|
self.root = root
|
|
self.root.title("CPU / RAM Monitor")
|
|
|
|
self.tracker = Tracker()
|
|
|
|
# --- Header card ---
|
|
card_bg = "#23272f"
|
|
card_fg = "#ffffff"
|
|
accent = "#4fc3f7"
|
|
|
|
header_frame = ttk.Frame(root)
|
|
header_frame.pack(fill=tk.X, padx=10, pady=(10, 0))
|
|
|
|
self.cpu_label = ttk.Label(
|
|
header_frame,
|
|
text=f"CPU: --%",
|
|
font=("Segoe UI", 20, "bold"),
|
|
foreground=accent,
|
|
)
|
|
self.cpu_label.pack(side=tk.LEFT, padx=20)
|
|
|
|
self.ram_label = ttk.Label(
|
|
header_frame,
|
|
text=f"RAM: --%",
|
|
font=("Segoe UI", 20, "bold"),
|
|
foreground="#81c784",
|
|
)
|
|
self.ram_label.pack(side=tk.LEFT, padx=20)
|
|
|
|
self.net_label = ttk.Label(
|
|
header_frame,
|
|
text=f"Net: ↓ --",
|
|
font=("Segoe UI", 20, "bold"),
|
|
foreground="#ffb74d",
|
|
)
|
|
self.net_label.pack(side=tk.LEFT, padx=20)
|
|
|
|
self.disk_label = ttk.Label(
|
|
header_frame,
|
|
text=f"Disk: --%",
|
|
font=("Segoe UI", 20, "bold"),
|
|
foreground="#ba68c8",
|
|
)
|
|
self.disk_label.pack(side=tk.LEFT, padx=20)
|
|
|
|
self.fig = None
|
|
|
|
if HAVE_MPL:
|
|
# --- Plot ---
|
|
self.fig = Figure(figsize=(14, 3), facecolor=card_bg)
|
|
ax_cpu = self.fig.add_subplot(1, 3, 1, facecolor=card_bg)
|
|
ax_ram = self.fig.add_subplot(1, 3, 2, facecolor=card_bg)
|
|
ax_disk = self.fig.add_subplot(1, 3, 3, facecolor=card_bg)
|
|
|
|
ax_cpu.set_title("CPU %", color=card_fg, fontsize=11)
|
|
ax_ram.set_title("RAM %", color=card_fg, fontsize=11)
|
|
ax_disk.set_title("Disk %", color=card_fg, fontsize=11)
|
|
ax_cpu.set_ylim(0, 100)
|
|
ax_ram.set_ylim(0, 100)
|
|
ax_disk.set_ylim(0, 100)
|
|
|
|
ax_cpu.tick_params(colors="#aaa")
|
|
ax_ram.tick_params(colors="#aaa")
|
|
ax_disk.tick_params(colors="#aaa")
|
|
for label in ax_cpu.get_yticklabels():
|
|
label.set_color("#aaa")
|
|
for label in ax_ram.get_yticklabels():
|
|
label.set_color("#aaa")
|
|
for label in ax_disk.get_yticklabels():
|
|
label.set_color("#aaa")
|
|
for line in ax_cpu.spines.values():
|
|
line.set_color("#3a3f4a")
|
|
for line in ax_ram.spines.values():
|
|
line.set_color("#3a3f4a")
|
|
for line in ax_disk.spines.values():
|
|
line.set_color("#3a3f4a")
|
|
|
|
self.cpu_line, = ax_cpu.plot(
|
|
[], [], color="#ef5350", linewidth=1.5,
|
|
marker="", markerfacecolor="#ef5350"
|
|
)
|
|
self.ram_line, = ax_ram.plot(
|
|
[], [], color="#66bb6a", linewidth=1.5,
|
|
marker="", markerfacecolor="#66bb6a"
|
|
)
|
|
self.disk_line, = ax_disk.plot(
|
|
[], [], color="#ba68c8", linewidth=1.5,
|
|
marker="", markerfacecolor="#ba68c8"
|
|
)
|
|
|
|
ax_cpu.axhline(y=80, color="#ef5350", linestyle="--", alpha=0.4)
|
|
|
|
canvas = FigureCanvasTkAgg(self.fig, master=root)
|
|
canvas.draw()
|
|
canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
|
|
|
# --- Footer status ---
|
|
status_frame = ttk.Frame(root)
|
|
status_frame.pack(fill=tk.X, padx=10, pady=(0, 10))
|
|
|
|
self.status_label = ttk.Label(
|
|
status_frame,
|
|
text="Monitoring...",
|
|
font=("Segoe UI", 9),
|
|
foreground="#888",
|
|
)
|
|
self.status_label.pack(side=tk.LEFT)
|
|
|
|
stop_btn = ttk.Button(
|
|
status_frame,
|
|
text="Stop",
|
|
command=self._stop,
|
|
width=10,
|
|
)
|
|
stop_btn.pack(side=tk.RIGHT, padx=(10, 0))
|
|
|
|
self._running = True
|
|
self._refresh()
|
|
|
|
def _refresh(self):
|
|
if not self._running:
|
|
return
|
|
cpu = self.tracker.cpu_percent()
|
|
ram = psutil.virtual_memory().percent
|
|
disk = psutil.disk_usage('/').percent
|
|
App._last_cpu = cpu
|
|
self.cpu_data.append(cpu)
|
|
self.ram_data.append(ram)
|
|
|
|
if len(self.cpu_data) > MAX_POINTS:
|
|
self.cpu_data = self.cpu_data[-MAX_POINTS:]
|
|
if len(self.ram_data) > MAX_POINTS:
|
|
self.ram_data = self.ram_data[-MAX_POINTS:]
|
|
|
|
self.cpu_label.config(text=f"CPU: {cpu:5.1f}%")
|
|
self.ram_label.config(text=f"RAM: {ram:5.1f}%")
|
|
self.disk_label.config(text=f"Disk: {disk:5.1f}%")
|
|
|
|
if HAVE_MPL and self.fig:
|
|
ax_cpu = self.fig.axes[0]
|
|
ax_ram = self.fig.axes[1]
|
|
ax_disk = self.fig.axes[2]
|
|
|
|
self.cpu_line.set_data(range(len(self.cpu_data)), self.cpu_data)
|
|
self.ram_line.set_data(range(len(self.ram_data)), self.ram_data)
|
|
|
|
ax_cpu.set_xlim(0, MAX_POINTS)
|
|
ax_ram.set_xlim(0, MAX_POINTS)
|
|
ax_disk.set_xlim(0, MAX_POINTS)
|
|
|
|
self.fig.canvas.draw_idle()
|
|
|
|
self.root.after(UPDATE_INTERVAL, self._refresh)
|
|
|
|
def _stop(self):
|
|
self._running = False
|
|
self.status_label.config(text="Stopped.")
|
|
self.root.destroy()
|
|
if HAVE_INDICATOR:
|
|
GLib.idle_add(Gtk.main_quit)
|
|
|
|
|
|
def main():
|
|
root = tk.Tk()
|
|
app = App(root)
|
|
|
|
_tray = None
|
|
if HAVE_INDICATOR:
|
|
_tray = SysTrayIndicator(app)
|
|
root.bind('<Destroy>', lambda e: GLib.idle_add(Gtk.main_quit))
|
|
|
|
root.mainloop()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|