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 da8d441..e53c908 100644 Binary files a/debian/cpu-monitor/usr/share/doc/cpu-monitor/changelog.gz and b/debian/cpu-monitor/usr/share/doc/cpu-monitor/changelog.gz differ diff --git a/debian/cpu-monitor/usr/share/icons/hicolor/48x48/apps/cpu-monitor.png b/debian/cpu-monitor/usr/share/icons/hicolor/48x48/apps/cpu-monitor.png new file mode 100644 index 0000000..8fef48a Binary files /dev/null and b/debian/cpu-monitor/usr/share/icons/hicolor/48x48/apps/cpu-monitor.png differ diff --git a/debian/files b/debian/files index 669bd29..1aabd31 100644 --- a/debian/files +++ b/debian/files @@ -1,2 +1,2 @@ -cpu-monitor_1.0_all.deb python optional -cpu-monitor_1.0_arm64.buildinfo python optional +cpu-monitor_1.1_all.deb utils optional +cpu-monitor_1.1_arm64.buildinfo utils optional