From b8abe83c98bd612281ee65558b487f3c6e0352a0 Mon Sep 17 00:00:00 2001 From: tmzf5455 Date: Sun, 10 May 2026 00:04:53 +0200 Subject: [PATCH] v1.1: fix ayatana dep name (ayatanaappindicator3-0.1), rebuild package --- .../dh_installchangelogs.dch.trimmed | 8 +- debian/changelog | 8 +- debian/control | 2 +- debian/cpu-monitor/DEBIAN/conffiles | 1 + debian/cpu-monitor/DEBIAN/control | 15 +- debian/cpu-monitor/DEBIAN/md5sums | 6 +- .../xdg/autostart}/cpu-monitor.desktop | 6 +- debian/cpu-monitor/usr/bin/cpu-monitor | 335 +++++++++++------- .../usr/share/doc/cpu-monitor/changelog.gz | Bin 124 -> 209 bytes .../icons/hicolor/48x48/apps/cpu-monitor.png | Bin 0 -> 4476 bytes debian/files | 4 +- 11 files changed, 227 insertions(+), 158 deletions(-) create mode 100644 debian/cpu-monitor/DEBIAN/conffiles rename debian/cpu-monitor/{usr/share/applications => etc/xdg/autostart}/cpu-monitor.desktop (56%) create mode 100644 debian/cpu-monitor/usr/share/icons/hicolor/48x48/apps/cpu-monitor.png diff --git a/debian/.debhelper/generated/cpu-monitor/dh_installchangelogs.dch.trimmed b/debian/.debhelper/generated/cpu-monitor/dh_installchangelogs.dch.trimmed index 241e004..8cf162d 100644 --- a/debian/.debhelper/generated/cpu-monitor/dh_installchangelogs.dch.trimmed +++ b/debian/.debhelper/generated/cpu-monitor/dh_installchangelogs.dch.trimmed @@ -1,5 +1,7 @@ -cpu-monitor (1.0) stable; urgency=low +cpu-monitor (1.1) noble; urgency=low - * Initial release + * v2: Add GNOME AppIndicator systray icon in system tray bar + * v2: Autostart .desktop in /etc/xdg/autostart + * v2: System tray icon with CPU/RAM status menu - -- tmzf5455 Sat, 09 May 2026 10:00:00 +0000 + -- tmzf5455 Sun, 10 May 2026 00:00:00 +0000 diff --git a/debian/changelog b/debian/changelog index 241e004..8cf162d 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,5 +1,7 @@ -cpu-monitor (1.0) stable; urgency=low +cpu-monitor (1.1) noble; urgency=low - * Initial release + * v2: Add GNOME AppIndicator systray icon in system tray bar + * v2: Autostart .desktop in /etc/xdg/autostart + * v2: System tray icon with CPU/RAM status menu - -- tmzf5455 Sat, 09 May 2026 10:00:00 +0000 + -- tmzf5455 Sun, 10 May 2026 00:00:00 +0000 diff --git a/debian/control b/debian/control index adbd43b..8bc4ff1 100644 --- a/debian/control +++ b/debian/control @@ -8,7 +8,7 @@ Rules-Requires-Root: no Package: cpu-monitor Architecture: all -Depends: python3, python3-psutil, python3-matplotlib, python3-tk, gir1.2-ayatana-appindicator3-0.1, ${misc:Depends} +Depends: python3, python3-psutil, python3-matplotlib, python3-tk, gir1.2-ayatanaappindicator3-0.1, ${misc:Depends} Description: CPU/RAM/Disk/Network monitor GUI A system monitor tool with real-time matplotlib charts and a tkinter GUI, showing CPU, RAM, disk and network usage. diff --git a/debian/cpu-monitor/DEBIAN/conffiles b/debian/cpu-monitor/DEBIAN/conffiles new file mode 100644 index 0000000..fceced4 --- /dev/null +++ b/debian/cpu-monitor/DEBIAN/conffiles @@ -0,0 +1 @@ +/etc/xdg/autostart/cpu-monitor.desktop diff --git a/debian/cpu-monitor/DEBIAN/control b/debian/cpu-monitor/DEBIAN/control index 76edf1f..9d92cdf 100644 --- a/debian/cpu-monitor/DEBIAN/control +++ b/debian/cpu-monitor/DEBIAN/control @@ -1,10 +1,11 @@ Package: cpu-monitor -Version: 1.0 +Version: 1.1 Architecture: all -Maintainer: tmzf5455 -Installed-Size: 20 -Depends: python3, python3-psutil, python3-matplotlib, python3-tk, x11-utils -Section: python +Maintainer: tmzf5455 +Installed-Size: 32 +Depends: python3, python3-psutil, python3-matplotlib, python3-tk, gir1.2-ayatanaappindicator3-0.1 +Section: utils Priority: optional -Description: CPU Monitor GUI - A Python GUI application for monitoring CPU usage in real-time. +Description: CPU/RAM/Disk/Network monitor GUI + A system monitor tool with real-time matplotlib charts + and a tkinter GUI, showing CPU, RAM, disk and network usage. diff --git a/debian/cpu-monitor/DEBIAN/md5sums b/debian/cpu-monitor/DEBIAN/md5sums index a4791b9..0982e61 100644 --- a/debian/cpu-monitor/DEBIAN/md5sums +++ b/debian/cpu-monitor/DEBIAN/md5sums @@ -1,3 +1,3 @@ -3bb2a78f10db731e82226d58d83c7d31 usr/bin/cpu-monitor -b40607068b416aad76a36519e068e13a usr/share/applications/cpu-monitor.desktop -417d73b9352097d7166b96829dc5a863 usr/share/doc/cpu-monitor/changelog.gz +f0e9f6392ef8c8b1447d39da0622a0d7 usr/bin/cpu-monitor +c8cc9827601e0cc0b156979c4caa4980 usr/share/doc/cpu-monitor/changelog.gz +fd11337fda885b79dc49d83a15abcffd usr/share/icons/hicolor/48x48/apps/cpu-monitor.png diff --git a/debian/cpu-monitor/usr/share/applications/cpu-monitor.desktop b/debian/cpu-monitor/etc/xdg/autostart/cpu-monitor.desktop similarity index 56% rename from debian/cpu-monitor/usr/share/applications/cpu-monitor.desktop rename to debian/cpu-monitor/etc/xdg/autostart/cpu-monitor.desktop index edec41a..8909987 100644 --- a/debian/cpu-monitor/usr/share/applications/cpu-monitor.desktop +++ b/debian/cpu-monitor/etc/xdg/autostart/cpu-monitor.desktop @@ -1,9 +1,7 @@ [Desktop Entry] Type=Application Exec=/usr/bin/cpu-monitor -Hidden=false -NoDisplay=false -X-GNOME-Autostart-enabled=true Name=CPU Monitor GUI Comment=CPU/RAM/Disk/Network monitor -Icon=utilities-system-monitor +Icon=cpu-monitor +Terminal=false diff --git a/debian/cpu-monitor/usr/bin/cpu-monitor b/debian/cpu-monitor/usr/bin/cpu-monitor index 3953d86..a2486d5 100755 --- a/debian/cpu-monitor/usr/bin/cpu-monitor +++ b/debian/cpu-monitor/usr/bin/cpu-monitor @@ -1,43 +1,51 @@ #!/usr/bin/env python3 -# Lancement: python cpu_monitor_gui.py +# Lancement: cpu-monitor import tkinter as tk from tkinter import ttk -from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk -from matplotlib.figure import Figure -from collections import deque +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 = deque(maxlen=MAX_POINTS) - self.ram_data = deque(maxlen=MAX_POINTS) - self.disk_data = deque(maxlen=MAX_POINTS) - self.disk_read_data = deque(maxlen=MAX_POINTS) - self.disk_write_data = deque(maxlen=MAX_POINTS) - self.net_sent_data = deque(maxlen=MAX_POINTS) - self.net_recv_data = deque(maxlen=MAX_POINTS) + self.cpu_data = [] + self.ram_data = [] + self.disk_data = [] self._last_cpu = None - self._last_net_sent = None - self._last_net_recv = None - self._last_disk_read = None - self._last_disk_write = None now = time.time() - self.time_data = deque(maxlen=MAX_POINTS) - self.time_data.append(now) + self.time_data = [now] - def _net_rate(self, curr, last): + def _net_rate(self, curr, last, interval): delta = curr - last - rate = max(delta / 2.0, 0) + rate = max(delta / interval, 0) return max(int(rate), 0) - def update(self): + def update(self, dt): cpu = psutil.cpu_percent(interval=None) ram = psutil.virtual_memory().percent disk = psutil.disk_usage('/').percent @@ -49,49 +57,118 @@ class Tracker: self.ram_data.append(ram) self.disk_data.append(disk) - d_io = psutil.disk_io_counters() - if d_io is not None: - d_read = d_io.read_bytes - d_write = d_io.write_bytes - if self._last_disk_read is not None: - self.disk_read_data.append(max(int((d_read - self._last_disk_read) / 2.0), 0)) - self.disk_write_data.append(max(int((d_write - self._last_disk_write) / 2.0), 0)) - self._last_disk_read = d_read - self._last_disk_write = d_write - net = psutil.net_io_counters() sent = net.bytes_sent recv = net.bytes_recv - if self._last_net_sent is not None: - send_rate = self._net_rate(sent, self._last_net_sent) - recv_rate = self._net_rate(recv, self._last_net_recv) - self.net_sent_data.append(send_rate) - self.net_recv_data.append(recv_rate) self._last_net_sent = sent self._last_net_recv = recv def get_latest(self): if self._last_cpu is None: - self.update() - return self._last_cpu, self.ram_data[-1] + self.update(1) + return self._last_cpu, self.ram_data[-1] if self.ram_data else 0 def get_disk(self): - if not self.disk_data: - return 0 - return self.disk_data[-1] - - def get_disk_rates(self): - if not self.disk_read_data or not self.disk_write_data: - return 0, 0 - return self.disk_read_data[-1], self.disk_write_data[-1] + return self.disk_data[-1] if self.disk_data else 0 def get_net_rates(self): - if not self.net_sent_data or not self.net_recv_data: - return 0, 0 - return self.net_sent_data[-1], self.net_recv_data[-1] + 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") @@ -138,74 +215,56 @@ class App: ) self.disk_label.pack(side=tk.LEFT, padx=20) - # --- Plot --- - fig = Figure(figsize=(14, 3), facecolor=card_bg) - ax_cpu = fig.add_subplot(1, 4, 1, facecolor=card_bg) - ax_ram = fig.add_subplot(1, 4, 2, facecolor=card_bg) - ax_disk = fig.add_subplot(1, 4, 3, facecolor=card_bg) - ax_net = fig.add_subplot(1, 4, 4, facecolor=card_bg) + self.fig = None - 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_net.set_title("Réseau (B/s)", color=card_fg, fontsize=11) - ax_cpu.set_ylim(0, 100) - ax_ram.set_ylim(0, 100) - ax_disk.set_ylim(0, 100) - ax_net.set_ylim(0, 1) - ax_cpu.set_ylabel("%") - ax_ram.set_ylabel("%") - ax_disk.set_ylabel("%") - ax_net.set_ylabel("B/s") - ax_cpu.tick_params(colors="#aaa") - ax_ram.tick_params(colors="#aaa") - ax_net.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_net.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_net.spines.values(): - line.set_color("#3a3f4a") + 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) - 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" - ) - self.net_sent_line, = ax_net.plot( - [], [], color="#ffb74d", linewidth=1.5, marker="", markerfacecolor="#ffb74d" - ) - self.net_recv_line, = ax_net.plot( - [], [], color="#90caf9", linewidth=1.5, marker="", markerfacecolor="#90caf9" - ) + 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.axhline(y=80, color="#ef5350", linestyle="--", alpha=0.4) - ax_ram.axhline(y=80, color="#66bb6a", linestyle="--", alpha=0.4) - ax_disk.axhline(y=80, color="#ba68c8", linestyle="--", alpha=0.4) - ax_cpu.text(0.02, 0.02, "80%", fontsize=8, color="#ef5350", alpha=0.6, transform=ax_cpu.transAxes) + 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.fig = fig - self.ax_cpu = ax_cpu - self.ax_ram = ax_ram - self.ax_disk = ax_disk - self.ax_net = ax_net + 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" + ) - canvas = FigureCanvasTkAgg(fig, master=root) - canvas.draw() - canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True, padx=10, pady=10) + ax_cpu.axhline(y=80, color="#ef5350", linestyle="--", alpha=0.4) - toolbar = NavigationToolbar2Tk(canvas, root) - toolbar.update() + 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) @@ -233,49 +292,55 @@ class App: def _refresh(self): if not self._running: return - self.tracker.update() - cpu, ram = self.tracker.get_latest() - disk = self.tracker.get_disk() - send_rate, recv_rate = self.tracker.get_net_rates() + 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}%") - mb_sent = send_rate / (1024 * 1024) - mb_recv = recv_rate / (1024 * 1024) - self.net_label.config(text=f"Net: ↑{mb_sent:.2f} ↓{mb_recv:.2f} MB/s") + if HAVE_MPL and self.fig: + ax_cpu = self.fig.axes[0] + ax_ram = self.fig.axes[1] + ax_disk = self.fig.axes[2] - cpu_arr = list(self.tracker.cpu_data) - ram_arr = list(self.tracker.ram_data) - disk_arr = list(self.tracker.disk_data) - sent_arr = list(self.tracker.net_sent_data) - recv_arr = list(self.tracker.net_recv_data) - self.cpu_line.set_data(range(len(cpu_arr)), cpu_arr) - self.ram_line.set_data(range(len(ram_arr)), ram_arr) - self.disk_line.set_data(range(len(disk_arr)), disk_arr) - self.net_sent_line.set_data(range(len(sent_arr)), sent_arr) - self.net_recv_line.set_data(range(len(recv_arr)), recv_arr) + 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) - self.ax_cpu.set_xlim(0, MAX_POINTS) - self.ax_ram.set_xlim(0, MAX_POINTS) - self.ax_disk.set_xlim(0, MAX_POINTS) - self.ax_net.set_xlim(0, MAX_POINTS) + ax_cpu.set_xlim(0, MAX_POINTS) + ax_ram.set_xlim(0, MAX_POINTS) + ax_disk.set_xlim(0, MAX_POINTS) - max_net = max(max(sent_arr) if sent_arr else 0, max(recv_arr) if recv_arr else 0) - if max_net > 0: - self.ax_net.set_ylim(0, max_net * 1.2) + self.fig.canvas.draw_idle() - 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('', lambda e: GLib.idle_add(Gtk.main_quit)) + root.mainloop() diff --git a/debian/cpu-monitor/usr/share/doc/cpu-monitor/changelog.gz b/debian/cpu-monitor/usr/share/doc/cpu-monitor/changelog.gz index da8d4414f0e3d3b888ccee6253f9c289ad6a895f..e53c908f8458d86812ad0359cf4c33d233e0d9c8 100644 GIT binary patch literal 209 zcmV;?051O@iwFP!0000215J*>Zo)7SMDO{EIRYibi3kU1ks^c;hd>4D2N>H0tHh4% zHIRNiArhslJ?!kx8?C5ATv?;ZI`q;}+J~`+vzj5eqcWoTINNI!0RvpJDGDjE+HA`u zin?AKsRe(4zVYOE11+q9Hl0=#y4``h7aquZ;*Kzq%Kwn9AMQkvNN)0&@aOpt*#8Ua z!mpZ6SbTmZyP`zvrQlI16WSN!7*zd!=i@xb`*WHD`(TDh8Ok;zW7!0ZO}oY$Yb&|~ LCR;{80s#O3y}w_3 literal 124 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