diff --git a/.gitignore b/.gitignore index 26e2ff7..8a7a0b4 100644 --- a/.gitignore +++ b/.gitignore @@ -139,9 +139,15 @@ debian/debhelper-build-stamp debian/files debian/coremon/ +# Release/installer directory (for manual release uploads) +installer/ + # Python build artifacts .pybuild/ +# Factory build artifacts +.factory/ + # Release notes (internal) RELEASE_NOTES_*.md diff --git a/AGENTS.md b/AGENTS.md index ba3c554..4f4e6a6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -13,8 +13,8 @@ CoreMon is a system monitoring tool for Zorin OS and Ubuntu-based distributions. ### Building Debian Package - Install build dependencies: `sudo apt install build-essential devscripts debhelper dh-python python3-all python3-setuptools` - Update changelog: `export DEBEMAIL="your-email@example.com" && dch --distribution unstable --increment "Description of changes"` -- Build package: `dpkg-buildpackage -us -uc` -- Copy .deb to project: `cp ../coremon_*.deb .` +- Build package: `./build-installer.sh` (builds and places packages in installer/ directory) +- Alternative manual build: `dpkg-buildpackage -us -uc` (outputs to parent directory) ### Testing - Run the application: `python3 coremon/main.py` @@ -51,6 +51,54 @@ CoreMon is a system monitoring tool for Zorin OS and Ubuntu-based distributions. - System tray requires appropriate indicator support - Build artifacts appear in parent directory by default +## Current Issues (November 2025) + +### Issue: Settings Persistence and Autostart Creation +**Status: RESOLVED - FIXED** ✅ +- **Fixed**: Settings now properly persist to config file when changed in UI +- **Fixed**: Autostart desktop file is created when "Start on login" is enabled +- **Root Causes Identified and Fixed**: + 1. **Missing Signal Connection**: `start_minimized_switch` was created but never connected to `on_setting_changed` + 2. **Logic Error**: `_previous_autostart_state` was set AFTER updating settings, making comparison always false + 3. **File Write Buffering**: Added `flush()` and `fsync()` to ensure immediate disk writes +- **Files Modified**: + - `coremon/main.py` - Added signal connection and fixed state tracking logic + - `coremon/main.py` - Enhanced `save_settings()` with immediate disk sync +- **Verified**: Settings persist across application restarts and autostart file is created/removed correctly + +### Recently Fixed: Autostart Cleanup During Uninstall +**Status: WORKING** ✅ +- **Fixed**: prerm script now properly detects user and cleans up configuration files during uninstall +- **Files Modified**: + - `debian/coremon.prerm` - Added proper user detection logic + - `prerm` - Fixed file paths to use actual user directory instead of $HOME +- **Verified**: Autostart desktop file and config directory are properly removed during package uninstall + +### Testing Status +- Installation: ✅ Working +- Application Launch: ✅ Working +- Default Config Creation: ✅ Working +- Settings Persistence: ✅ WORKING +- Autostart Creation: ✅ WORKING +- Autostart Cleanup: ✅ Working + +## Versioning Process +When making a new release: + +1. **Update version in setup.py**: Change the version string +2. **Update application version**: + - Update `__version__` variable in `coremon/main.py` + - Update About screen version display in `coremon/main.py` (search for "CoreMon vX.X.X") +3. **Update Debian changelog**: Use `dch --distribution unstable --increment "Description"` +4. **Build packages**: Run `./build-installer.sh` (creates universal installer in installer/ directory) +5. **Test installation**: Verify universal installer works and version displays correctly in About screen + +**Important**: Always keep version numbers synchronized across: +- `setup.py` (Python package version) +- `coremon/main.py` `__version__` variable +- `coremon/main.py` About screen display +- `debian/changelog` (Debian package version) + ## Git Workflow - Main branch: `main` - Commit format: Conventional commits preferred diff --git a/build-installer.sh b/build-installer.sh new file mode 100755 index 0000000..1f4c176 --- /dev/null +++ b/build-installer.sh @@ -0,0 +1,48 @@ +#!/bin/bash + +# CoreMon Installer Build Script +# This script builds universal Debian packages and places them in the installer directory + +set -e + +# Get version from setup.py +VERSION=$(grep "version=" setup.py | sed "s/.*version=['\"]\([^'\"]*\)['\"].*/\1/") + +echo "Building CoreMon universal Debian package (version $VERSION)..." + +# Create installer directory if it doesn't exist +mkdir -p installer + +# Clean up any existing build artifacts in parent directory +echo "Cleaning up existing build artifacts..." +rm -f ../coremon_*.deb ../coremon_*.changes ../coremon_*.buildinfo ../coremon_*.dsc ../coremon_*.tar.gz + +# Temporarily modify debian/control to force universal architecture +echo "Configuring for universal architecture..." +cp debian/control debian/control.backup +sed -i 's/Architecture: any/Architecture: all/' debian/control + +# Build the Debian package +echo "Building universal Debian package..." +dpkg-buildpackage -us -uc + +# Restore original debian/control +mv -f debian/control.backup debian/control + +# Move and rename the universal package +echo "Moving and renaming universal package..." +if ls ../coremon_*_all.deb 1> /dev/null 2>&1; then + mv -f ../coremon_*_all.deb "installer/coremon_1.0.2-1_x64_arm64.deb" +fi + +# Move other artifacts with original names +mv -f ../coremon_*.changes installer/ 2>/dev/null || true +mv -f ../coremon_*.buildinfo installer/ 2>/dev/null || true +mv -f ../coremon_*.dsc installer/ 2>/dev/null || true +mv -f ../coremon_*.tar.gz installer/ 2>/dev/null || true + +echo "Build complete! Universal installer file is in the installer/ directory:" +ls -la installer/ + +echo "" +echo "To install locally, run: sudo apt install ./installer/coremon_1.0.2-1_x64_arm64.deb" diff --git a/control b/control new file mode 100644 index 0000000..8f35bce --- /dev/null +++ b/control @@ -0,0 +1,13 @@ +Package: coremon +Version: 1.0.2-1 +Architecture: all +Maintainer: CoreMon Team +Installed-Size: 103 +Depends: python3-gi, python3-matplotlib, python3-numpy, python3-psutil, python3:any, gir1.2-gtk-3.0, gir1.2-appindicator3-0.1, libappindicator3-1, neofetch +Section: utils +Priority: optional +Homepage: https://github.com/yourusername/coremon +Description: system monitoring tool for Ubuntu/Zorin OS + CoreMon displays real-time CPU temperature and load with graphs, + system tray integration, and configurable thresholds. Designed for + Ubuntu-based distributions. diff --git a/coremon/main.py b/coremon/main.py index d1ded5d..3b76616 100644 --- a/coremon/main.py +++ b/coremon/main.py @@ -1,8 +1,9 @@ #!/usr/bin/env python3 -__version__ = "1.0.1" +__version__ = "1.0.2" import gi +import traceback gi.require_version("Gtk", "3.0") gi.require_version("AppIndicator3", "0.1") @@ -30,6 +31,7 @@ class CoreMonApp(Gtk.Application): def __init__(self): super().__init__(application_id=APP_ID, flags=Gio.ApplicationFlags.FLAGS_NONE) + self.start_minimized_arg = False # Default settings self.settings = { @@ -48,9 +50,23 @@ def __init__(self): } self.load_settings() + self.ensure_default_config() # Ensure config file exists self.setup_data_structures() self.setup_indicator() + def do_command_line(self, command_line): + """Handle command line arguments""" + args = command_line.get_arguments() + + # Check for --minimized flag + if "--minimized" in args: + self.start_minimized_arg = True + print("Starting minimized due to --minimized flag") + + # Activate the application + self.activate() + return 0 + def setup_data_structures(self): self.max_data_points = 60 # Number of points to keep in history self.cpu_count = psutil.cpu_count() @@ -106,6 +122,87 @@ def save_settings(self): with open(CONFIG_FILE, "w") as configfile: config.write(configfile) + configfile.flush() # Force write to disk + os.fsync(configfile.fileno()) # Ensure OS writes to disk + + def ensure_default_config(self): + """Ensure a default config file exists with proper settings""" + if not os.path.exists(CONFIG_FILE): + print("No configuration file found, creating default settings...") + self.save_settings() + print(f"Default configuration created at {CONFIG_FILE}") + + def setup_autostart(self, enable): + """Enable or disable autostart by creating/removing desktop file in ~/.config/autostart/""" + try: + autostart_dir = os.path.expanduser("~/.config/autostart") + autostart_file = os.path.join(autostart_dir, "coremon.desktop") + + if enable: + print("DEBUG: Setting up autostart...") + # Create autostart directory if it doesn't exist + os.makedirs(autostart_dir, exist_ok=True) + + # Get the path to the coremon executable + # Try to find it in common locations + possible_paths = [ + "/usr/bin/coremon", + "/usr/local/bin/coremon", + os.path.expanduser("~/bin/coremon"), + ] + + coremon_path = None + for path in possible_paths: + if os.path.isfile(path): + coremon_path = path + print(f"DEBUG: Found executable at {path}") + break + + # Fallback to python3 -m coremon if no executable found + if not coremon_path: + coremon_path = "python3 -m coremon" + print("DEBUG: Using fallback python3 -m coremon") + + # Build the exec command with --minimized flag if start_minimized is enabled + exec_command = coremon_path + if self.settings.get("start_minimized", False): + exec_command += " --minimized" + print(f"DEBUG: Adding --minimized flag, exec: {exec_command}") + + # Create desktop entry content + desktop_content = f"""[Desktop Entry] +Type=Application +Name=CoreMon +Comment=System monitoring tool +Exec={exec_command} +Icon=temperature +Terminal=false +Categories=System;Monitor; +X-GNOME-Autostart-enabled=true +""" + + # Write the autostart desktop file + print(f"DEBUG: Writing autostart file to {autostart_file}") + with open(autostart_file, "w") as f: + f.write(desktop_content) + + print(f"Autostart enabled: {autostart_file}") + + else: + print("DEBUG: Removing autostart...") + # Remove the autostart file if it exists + if os.path.exists(autostart_file): + print(f"DEBUG: Removing {autostart_file}") + os.remove(autostart_file) + print(f"Autostart disabled: removed {autostart_file}") + else: + print("DEBUG: Autostart file not found, nothing to remove") + + except Exception as e: + print(f"ERROR in setup_autostart: {e}") + # Don't crash the app - just log the error + # The setting will still be saved, but autostart might not work + print(f"WARNING: Autostart setup failed, but continuing...") def do_activate(self): # Show the window @@ -115,9 +212,15 @@ def do_activate(self): # Initialize menu label based on initial visibility self.update_menu_label() - if self.settings["start_minimized"]: + # Check both settings and command line argument for start minimized + should_start_minimized = self.settings["start_minimized"] or self.start_minimized_arg + + if should_start_minimized: + # Hide the window after it's shown for minimized start self.win.hide() + self.win.set_skip_taskbar_hint(True) self.show_hide_item.set_label("Show") + # else: window remains shown normally def setup_indicator(self): # Create system tray indicator with temperature icon @@ -341,6 +444,9 @@ def __init__(self, app): self.app.settings["window_width"], self.app.settings["window_height"] ) + # Track previous autostart state for async handling + self._previous_autostart_state = self.app.settings.get("autostart", False) + self.setup_ui() self.start_monitoring() @@ -623,6 +729,7 @@ def setup_ui(self): # Start minimized self.start_minimized_switch = Gtk.Switch() self.start_minimized_switch.set_active(self.app.settings["start_minimized"]) + self.start_minimized_switch.connect("notify::active", self.on_setting_changed) minimized_box = Gtk.Box(spacing=12) minimized_label = Gtk.Label(label="Start minimized:") minimized_box.pack_start(minimized_label, False, False, 0) @@ -632,17 +739,13 @@ def setup_ui(self): # Autostart self.autostart_switch = Gtk.Switch() self.autostart_switch.set_active(self.app.settings["autostart"]) + self.autostart_switch.connect("notify::active", self.on_setting_changed) autostart_box = Gtk.Box(spacing=12) autostart_label = Gtk.Label(label="Start on login:") autostart_box.pack_start(autostart_label, False, False, 0) autostart_box.pack_end(self.autostart_switch, False, False, 0) settings_box.pack_start(autostart_box, False, False, 0) - # Save button - save_button = Gtk.Button(label="Save Settings") - save_button.connect("clicked", self.on_save_settings) - settings_box.pack_end(save_button, False, False, 0) - # System Info tab system_info_box = Gtk.Box( orientation=Gtk.Orientation.VERTICAL, spacing=12, margin=12 @@ -720,8 +823,13 @@ def setup_ui(self): notebook.append_page(about_box, Gtk.Label(label="About")) about_label = Gtk.Label() + # Get current date and time for build info + from datetime import datetime + build_datetime = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + about_label.set_markup( - "CoreMon v1.0\n\n" + f"CoreMon v1.0.2\n\n" + f"Built: {build_datetime}\n\n" "A simple temperature and load monitor for Ubuntu based OS.\n" "Designed to give you a quick view of your temps and load.\n\n" "© 2025 CoreMon Project" @@ -888,61 +996,113 @@ def format_uptime(self, seconds): return f"{hours:02d}:{minutes:02d}:{secs:02d}" def on_setting_changed(self, widget): - # Apply settings immediately - self.app.settings["update_interval"] = self.interval_spin.get_value_as_int() - self.app.settings["temperature_unit"] = self.unit_combo.get_active_id() - self.app.settings["temp_threshold"] = ( - self.temp_threshold_spin.get_value_as_int() - ) - self.app.settings["load_threshold"] = ( - self.load_threshold_spin.get_value_as_int() - ) - self.app.settings["cores_to_monitor"] = self.core_combo.get_active_id() - self.app.settings["show_individual_cores"] = ( - self.show_individual_cores_switch.get_active() - ) - self.app.settings["smooth_graphs"] = self.smooth_graphs_switch.get_active() - self.app.settings["dashboard_avg_only"] = ( - self.dashboard_avg_only_switch.get_active() - ) - self.app.settings["start_minimized"] = self.start_minimized_switch.get_active() - self.app.settings["autostart"] = self.autostart_switch.get_active() - - # Save to config file - self.app.save_settings() - - # Clear data to force refresh with new settings - self.app.time_data.clear() - for i in range(self.app.cpu_count): - self.app.temp_data[i].clear() - self.app.load_data[i].clear() - - def on_save_settings(self, widget): - # Save window size - width, height = self.get_size() - self.app.settings["window_width"] = width - self.app.settings["window_height"] = height - - # Save to config file - self.app.save_settings() - - # Show confirmation - dialog = Gtk.MessageDialog( - parent=self, - flags=0, - message_type=Gtk.MessageType.INFO, - buttons=Gtk.ButtonsType.OK, - text="Settings saved", - ) - dialog.format_secondary_text( - "Settings are applied immediately and will persist after restarting CoreMon." - ) - dialog.run() - dialog.destroy() + try: + print(f"DEBUG: on_setting_changed called with widget: {type(widget)}") + + # Store the OLD autostart state BEFORE updating settings + old_autostart_state = self.app.settings.get("autostart", False) + + # Apply settings immediately + self.app.settings["update_interval"] = self.interval_spin.get_value_as_int() + self.app.settings["temperature_unit"] = self.unit_combo.get_active_id() + self.app.settings["temp_threshold"] = ( + self.temp_threshold_spin.get_value_as_int() + ) + self.app.settings["load_threshold"] = ( + self.load_threshold_spin.get_value_as_int() + ) + self.app.settings["cores_to_monitor"] = self.core_combo.get_active_id() + self.app.settings["show_individual_cores"] = ( + self.show_individual_cores_switch.get_active() + ) + self.app.settings["smooth_graphs"] = self.smooth_graphs_switch.get_active() + self.app.settings["dashboard_avg_only"] = ( + self.dashboard_avg_only_switch.get_active() + ) + self.app.settings["start_minimized"] = self.start_minimized_switch.get_active() + + print(f"DEBUG: Setting autostart from switch: {self.autostart_switch.get_active()}") + self.app.settings["autostart"] = self.autostart_switch.get_active() + + print(f"DEBUG: Current settings - start_minimized: {self.app.settings['start_minimized']}, autostart: {self.app.settings['autostart']}") + + # Save to config file + self.app.save_settings() + print("DEBUG: Settings saved successfully") + + # Handle autostart setting change in background thread to prevent UI blocking + if self.app.settings["autostart"] != old_autostart_state: + print(f"DEBUG: Autostart state changed from {old_autostart_state} to {self.app.settings['autostart']}") + self._handle_autostart_change_async() + else: + print("DEBUG: Autostart state unchanged, skipping async handling") + + # Update tracking variable AFTER the comparison + self._previous_autostart_state = self.app.settings["autostart"] + + # Clear data to force refresh with new settings + self.app.time_data.clear() + for i in range(self.app.cpu_count): + self.app.temp_data[i].clear() + self.app.load_data[i].clear() + + print("DEBUG: on_setting_changed completed successfully") + + except Exception as e: + print(f"ERROR in on_setting_changed: {e}") + print(f"ERROR traceback: {traceback.format_exc()}") + # Don't crash the app - just log the error + print("WARNING: Settings change failed, but continuing...") def start_monitoring(self): self.update_ui() + def _handle_autostart_change_async(self): + """Handle autostart changes in background thread to prevent UI blocking""" + from gi.repository import GLib + + def autostart_worker(): + try: + print(f"DEBUG: Starting background autostart setup for {self.app.settings['autostart']}") + + # Give immediate UI feedback + GLib.idle_add(self._show_autostart_feedback, "Setting up autostart...") + + # Call the actual setup method + self.app.setup_autostart(self.app.settings["autostart"]) + + # Update previous state + self._previous_autostart_state = self.app.settings["autostart"] + + # Give completion feedback + GLib.idle_add(self._show_autostart_feedback, "Autostart setup complete") + GLib.timeout_add(2000, self._hide_autostart_feedback) # Hide after 2 seconds + + print("DEBUG: Background autostart setup complete") + + except Exception as e: + print(f"ERROR in background autostart: {e}") + GLib.idle_add(self._show_autostart_feedback, f"Autostart error: {str(e)}") + GLib.timeout_add(3000, self._hide_autostart_feedback) + self._previous_autostart_state = not self.app.settings["autostart"] # Revert state + + return False # Stop the timeout + + # Start the background thread + GLib.timeout_add(50, autostart_worker) # Small delay to let UI update first + + def _show_autostart_feedback(self, message): + """Show immediate feedback to user during autostart operations""" + # For now, just print to console - you could add a status label later + print(f"AUTOSTART: {message}") + # You could also update a status label or show a notification here + return False # Stop idle callback + + def _hide_autostart_feedback(self): + """Hide autostart feedback""" + print("AUTOSTART: Operation complete") + return False # Stop timeout callback + def update_ui(self): # Get current CPU data temps = self.app.get_cpu_temps() diff --git a/debian/.debhelper/generated/coremon/dh_installchangelogs.dch.trimmed b/debian/.debhelper/generated/coremon/dh_installchangelogs.dch.trimmed index eea644d..373250f 100644 --- a/debian/.debhelper/generated/coremon/dh_installchangelogs.dch.trimmed +++ b/debian/.debhelper/generated/coremon/dh_installchangelogs.dch.trimmed @@ -1,3 +1,20 @@ +coremon (1.0.2-1) unstable; urgency=medium + + * Non-maintainer upload. + * Fix autostart functionality and remove redundant Save button + + -- Spencer Francisco Sun, 16 Nov 2025 11:28:22 -0600 + +coremon (1.0.1-1) unstable; urgency=high + + * Fix hardcoded development paths in desktop file and launch script + * Add universal CPU sensor support (Intel, AMD, ARM) + * Add neofetch as dependency for System Info functionality + * Fix critical bug preventing installation on non-development machines + * Closes: #1 + + -- Spencer Francisco Mon, 03 Nov 2025 06:22:39 +0000 + coremon (1.0.0-1.2) unstable; urgency=medium * Non-maintainer upload. diff --git a/debian/changelog b/debian/changelog index 8fe89dd..373250f 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,10 @@ +coremon (1.0.2-1) unstable; urgency=medium + + * Non-maintainer upload. + * Fix autostart functionality and remove redundant Save button + + -- Spencer Francisco Sun, 16 Nov 2025 11:28:22 -0600 + coremon (1.0.1-1) unstable; urgency=high * Fix hardcoded development paths in desktop file and launch script diff --git a/debian/coremon.postinst b/debian/coremon.postinst new file mode 100755 index 0000000..fdbecdf --- /dev/null +++ b/debian/coremon.postinst @@ -0,0 +1,18 @@ +#!/bin/sh +set -e + +# Automatically added by dh_python3 +if command -v py3compile >/dev/null 2>&1; then + py3compile -p coremon +fi +if command -v pypy3compile >/dev/null 2>&1; then + pypy3compile -p coremon || true +fi + +# End automatically added section + +echo "CoreMon installation complete!" +echo "Note: User configuration will be created when you first run coremon." + +#DEBHELPER# +exit 0 diff --git a/debian/coremon.postinst.debhelper b/debian/coremon.postinst.debhelper index 195712e..d2311f6 100644 --- a/debian/coremon.postinst.debhelper +++ b/debian/coremon.postinst.debhelper @@ -1,10 +1,10 @@ # Automatically added by dh_python3 if command -v py3compile >/dev/null 2>&1; then - py3compile -p coremon:amd64 + py3compile -p coremon fi if command -v pypy3compile >/dev/null 2>&1; then - pypy3compile -p coremon:amd64 || true + pypy3compile -p coremon || true fi # End automatically added section diff --git a/debian/coremon.prerm b/debian/coremon.prerm new file mode 100755 index 0000000..5c73c92 --- /dev/null +++ b/debian/coremon.prerm @@ -0,0 +1,36 @@ +#!/bin/sh +set -e + +echo "Cleaning up CoreMon configuration..." + +# Get the actual user who initiated the package removal +# When running as root during package removal, we need to find the original user +if [ -n "$SUDO_USER" ]; then + REAL_USER="$SUDO_USER" +elif [ -n "$USER" ] && [ "$USER" != "root" ]; then + REAL_USER="$USER" +else + # Try to find the user who owns the X session or a logged in user + REAL_USER=$(logname 2>/dev/null || echo "$(ls -la /home | head -n 2 | tail -n 1 | awk '{print $3}')") +fi + +# Remove autostart desktop file if it exists +AUTOSTART_FILE="/home/$REAL_USER/.config/autostart/coremon.desktop" +if [ -f "$AUTOSTART_FILE" ]; then + echo "Removing autostart desktop file for user $REAL_USER..." + rm -f "$AUTOSTART_FILE" +fi + +# Remove entire CoreMon configuration directory +CONFIG_DIR="/home/$REAL_USER/.config/coremon" +if [ -d "$CONFIG_DIR" ]; then + echo "Removing CoreMon configuration directory for user $REAL_USER..." + rm -rf "$CONFIG_DIR" +fi + +echo "CoreMon configuration cleanup complete." + +# Include debhelper cleanup (this is important!) +#DEBHELPER# + +exit 0 diff --git a/debian/coremon.prerm.debhelper b/debian/coremon.prerm.debhelper index de34a2f..491a58e 100644 --- a/debian/coremon.prerm.debhelper +++ b/debian/coremon.prerm.debhelper @@ -1,9 +1,9 @@ # Automatically added by dh_python3 if command -v py3clean >/dev/null 2>&1; then - py3clean -p coremon:amd64 + py3clean -p coremon else - dpkg -L coremon:amd64 | sed -En -e '/^(.*)\/(.+)\.py$/s,,rm "\1/__pycache__/\2".*,e' + dpkg -L coremon | sed -En -e '/^(.*)\/(.+)\.py$/s,,rm "\1/__pycache__/\2".*,e' find /usr/lib/python3/dist-packages/ -type d -name __pycache__ -empty -print0 | xargs --null --no-run-if-empty rmdir fi diff --git a/debian/coremon/DEBIAN/control b/debian/coremon/DEBIAN/control index 135be18..8258bfe 100644 --- a/debian/coremon/DEBIAN/control +++ b/debian/coremon/DEBIAN/control @@ -1,9 +1,9 @@ Package: coremon -Version: 1.0.0-1.2 -Architecture: amd64 +Version: 1.0.2-1 +Architecture: all Maintainer: CoreMon Team -Installed-Size: 93 -Depends: python3-gi, python3-matplotlib, python3-numpy, python3-psutil, python3:any, gir1.2-gtk-3.0, gir1.2-appindicator3-0.1, libappindicator3-1 +Installed-Size: 105 +Depends: python3-gi, python3-matplotlib, python3-numpy, python3-psutil, python3:any, gir1.2-gtk-3.0, gir1.2-appindicator3-0.1, libappindicator3-1, neofetch Section: utils Priority: optional Homepage: https://github.com/yourusername/coremon diff --git a/debian/coremon/DEBIAN/md5sums b/debian/coremon/DEBIAN/md5sums index 6ac24a4..ed8f95c 100644 --- a/debian/coremon/DEBIAN/md5sums +++ b/debian/coremon/DEBIAN/md5sums @@ -1,13 +1,13 @@ -a449be47dcdac81f427b2fb4220c3a21 usr/bin/coremon -7882fc2f0be00e66ab8107d07a659580 usr/bin/coremon.sh -57227d7ea180525e3df9f108693461fd usr/lib/python3/dist-packages/coremon-1.0.0.egg-info/PKG-INFO -68b329da9893e34099c7d8ad5cb9c940 usr/lib/python3/dist-packages/coremon-1.0.0.egg-info/dependency_links.txt -7b5945e91fac3e9e174395ee16f3e730 usr/lib/python3/dist-packages/coremon-1.0.0.egg-info/entry_points.txt -d41d8cd98f00b204e9800998ecf8427e usr/lib/python3/dist-packages/coremon-1.0.0.egg-info/requires.txt -d6b3addc3d572d81c8786a224f30db57 usr/lib/python3/dist-packages/coremon-1.0.0.egg-info/top_level.txt -1b0c142d7a2be19b442168f22ce78348 usr/lib/python3/dist-packages/coremon/main.py +a063dbcca9f5b9c7dbb74c45f2b8cf0a usr/bin/coremon +c6f6909bd8c899bb4410cde5e3438d8f usr/bin/coremon.sh +db3ff09f9797fa2fde01ede1b1c00a10 usr/lib/python3/dist-packages/coremon-1.0.2.egg-info/PKG-INFO +68b329da9893e34099c7d8ad5cb9c940 usr/lib/python3/dist-packages/coremon-1.0.2.egg-info/dependency_links.txt +7b5945e91fac3e9e174395ee16f3e730 usr/lib/python3/dist-packages/coremon-1.0.2.egg-info/entry_points.txt +cd849028afb2cb2a01717caec8bab65e usr/lib/python3/dist-packages/coremon-1.0.2.egg-info/requires.txt +d6b3addc3d572d81c8786a224f30db57 usr/lib/python3/dist-packages/coremon-1.0.2.egg-info/top_level.txt +4922aae580220a9071c0ffe5b1c0f57f usr/lib/python3/dist-packages/coremon/main.py 5e1f7bc6f276ecc2991614bc09420087 usr/lib/python3/dist-packages/cpumon/__init__.py 5134ab29569f4f8608a6c13c7fcf1cb7 usr/lib/python3/dist-packages/cpumon/main.py 68350531d35be40a469bd3065388940f usr/lib/python3/dist-packages/cpumon/setup.py -5a3a2777f837f98b3886594f0b4cb89c usr/share/applications/coremon.desktop -a73e370df467db924d4899321e1ca85c usr/share/doc/coremon/changelog.Debian.gz +b376166792deb340907cb0156965c890 usr/share/applications/coremon.desktop +099389bb212c1827b73dc7f786132e01 usr/share/doc/coremon/changelog.Debian.gz diff --git a/debian/coremon/DEBIAN/postinst b/debian/coremon/DEBIAN/postinst index 844e188..efdef3d 100755 --- a/debian/coremon/DEBIAN/postinst +++ b/debian/coremon/DEBIAN/postinst @@ -3,10 +3,26 @@ set -e # Automatically added by dh_python3 if command -v py3compile >/dev/null 2>&1; then - py3compile -p coremon:amd64 + py3compile -p coremon fi if command -v pypy3compile >/dev/null 2>&1; then - pypy3compile -p coremon:amd64 || true + pypy3compile -p coremon || true fi # End automatically added section + +echo "CoreMon installation complete!" +echo "Note: User configuration will be created when you first run coremon." + + +# Automatically added by dh_python3 +if command -v py3compile >/dev/null 2>&1; then + py3compile -p coremon +fi +if command -v pypy3compile >/dev/null 2>&1; then + pypy3compile -p coremon || true +fi + +# End automatically added section + +exit 0 diff --git a/debian/coremon/DEBIAN/prerm b/debian/coremon/DEBIAN/prerm index 71f6b5b..d3e0fec 100755 --- a/debian/coremon/DEBIAN/prerm +++ b/debian/coremon/DEBIAN/prerm @@ -1,12 +1,46 @@ #!/bin/sh set -e +echo "Cleaning up CoreMon configuration..." + +# Get the actual user who initiated the package removal +# When running as root during package removal, we need to find the original user +if [ -n "$SUDO_USER" ]; then + REAL_USER="$SUDO_USER" +elif [ -n "$USER" ] && [ "$USER" != "root" ]; then + REAL_USER="$USER" +else + # Try to find the user who owns the X session or a logged in user + REAL_USER=$(logname 2>/dev/null || echo "$(ls -la /home | head -n 2 | tail -n 1 | awk '{print $3}')") +fi + +# Remove autostart desktop file if it exists +AUTOSTART_FILE="/home/$REAL_USER/.config/autostart/coremon.desktop" +if [ -f "$AUTOSTART_FILE" ]; then + echo "Removing autostart desktop file for user $REAL_USER..." + rm -f "$AUTOSTART_FILE" +fi + +# Remove entire CoreMon configuration directory +CONFIG_DIR="/home/$REAL_USER/.config/coremon" +if [ -d "$CONFIG_DIR" ]; then + echo "Removing CoreMon configuration directory for user $REAL_USER..." + rm -rf "$CONFIG_DIR" +fi + +echo "CoreMon configuration cleanup complete." + +# Include debhelper cleanup (this is important!) + # Automatically added by dh_python3 if command -v py3clean >/dev/null 2>&1; then - py3clean -p coremon:amd64 + py3clean -p coremon else - dpkg -L coremon:amd64 | sed -En -e '/^(.*)\/(.+)\.py$/s,,rm "\1/__pycache__/\2".*,e' + dpkg -L coremon | sed -En -e '/^(.*)\/(.+)\.py$/s,,rm "\1/__pycache__/\2".*,e' find /usr/lib/python3/dist-packages/ -type d -name __pycache__ -empty -print0 | xargs --null --no-run-if-empty rmdir fi # End automatically added section + + +exit 0 diff --git a/debian/coremon/usr/bin/coremon b/debian/coremon/usr/bin/coremon index e6e6848..39d3272 100755 --- a/debian/coremon/usr/bin/coremon +++ b/debian/coremon/usr/bin/coremon @@ -1,10 +1,10 @@ #!/usr/bin/python3 -# EASY-INSTALL-ENTRY-SCRIPT: 'coremon==1.0.0','console_scripts','coremon' +# EASY-INSTALL-ENTRY-SCRIPT: 'coremon==1.0.2','console_scripts','coremon' import re import sys # for compatibility with easy_install; see #2198 -__requires__ = 'coremon==1.0.0' +__requires__ = 'coremon==1.0.2' try: from importlib.metadata import distribution @@ -30,4 +30,4 @@ globals().setdefault('load_entry_point', importlib_load_entry_point) if __name__ == '__main__': sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0]) - sys.exit(load_entry_point('coremon==1.0.0', 'console_scripts', 'coremon')()) + sys.exit(load_entry_point('coremon==1.0.2', 'console_scripts', 'coremon')()) diff --git a/debian/coremon/usr/bin/coremon.sh b/debian/coremon/usr/bin/coremon.sh index 800847e..791e9c8 100755 --- a/debian/coremon/usr/bin/coremon.sh +++ b/debian/coremon/usr/bin/coremon.sh @@ -1,3 +1,2 @@ #!/bin/bash -cd /home/spencer/Documents/GitHub/CascadeProjects/windsurf-project -python3 coremon/main.py +exec python3 -m coremon.main diff --git a/debian/coremon/usr/lib/python3/dist-packages/coremon-1.0.0.egg-info/PKG-INFO b/debian/coremon/usr/lib/python3/dist-packages/coremon-1.0.0.egg-info/PKG-INFO deleted file mode 100644 index f01d5a5..0000000 --- a/debian/coremon/usr/lib/python3/dist-packages/coremon-1.0.0.egg-info/PKG-INFO +++ /dev/null @@ -1,9 +0,0 @@ -Metadata-Version: 2.1 -Name: coremon -Version: 1.0.0 -Summary: A system monitoring tool for Zorin OS and Ubuntu -Home-page: https://github.com/yourusername/coremon -Author: CoreMon Team -Author-email: support@example.com -License: GPL-3.0 -Keywords: monitoring cpu temperature load system diff --git a/debian/coremon/usr/lib/python3/dist-packages/coremon-1.0.0.egg-info/dependency_links.txt b/debian/coremon/usr/lib/python3/dist-packages/coremon-1.0.0.egg-info/dependency_links.txt deleted file mode 100644 index 8b13789..0000000 --- a/debian/coremon/usr/lib/python3/dist-packages/coremon-1.0.0.egg-info/dependency_links.txt +++ /dev/null @@ -1 +0,0 @@ - diff --git a/debian/coremon/usr/lib/python3/dist-packages/coremon-1.0.0.egg-info/entry_points.txt b/debian/coremon/usr/lib/python3/dist-packages/coremon-1.0.0.egg-info/entry_points.txt deleted file mode 100644 index 1309f08..0000000 --- a/debian/coremon/usr/lib/python3/dist-packages/coremon-1.0.0.egg-info/entry_points.txt +++ /dev/null @@ -1,2 +0,0 @@ -[console_scripts] -coremon = coremon.main:main diff --git a/debian/coremon/usr/lib/python3/dist-packages/coremon-1.0.0.egg-info/requires.txt b/debian/coremon/usr/lib/python3/dist-packages/coremon-1.0.0.egg-info/requires.txt deleted file mode 100644 index e69de29..0000000 diff --git a/debian/coremon/usr/lib/python3/dist-packages/coremon-1.0.0.egg-info/top_level.txt b/debian/coremon/usr/lib/python3/dist-packages/coremon-1.0.0.egg-info/top_level.txt deleted file mode 100644 index bc92ee6..0000000 --- a/debian/coremon/usr/lib/python3/dist-packages/coremon-1.0.0.egg-info/top_level.txt +++ /dev/null @@ -1 +0,0 @@ -cpumon diff --git a/debian/coremon/usr/lib/python3/dist-packages/coremon/main.py b/debian/coremon/usr/lib/python3/dist-packages/coremon/main.py index e70ff72..3b76616 100644 --- a/debian/coremon/usr/lib/python3/dist-packages/coremon/main.py +++ b/debian/coremon/usr/lib/python3/dist-packages/coremon/main.py @@ -1,16 +1,21 @@ #!/usr/bin/env python3 +__version__ = "1.0.2" + import gi -gi.require_version('Gtk', '3.0') -gi.require_version('AppIndicator3', '0.1') -gi.require_version('Pango', '1.0') +import traceback + +gi.require_version("Gtk", "3.0") +gi.require_version("AppIndicator3", "0.1") +gi.require_version("Pango", "1.0") from gi.repository import Gtk, GLib, Gio, AppIndicator3, Pango, Gdk import os import threading import time import psutil import matplotlib -matplotlib.use('Agg') # Use non-interactive backend + +matplotlib.use("Agg") # Use non-interactive backend from matplotlib.backends.backend_gtk3agg import FigureCanvasGTK3Agg as FigureCanvas from matplotlib.figure import Figure import numpy as np @@ -18,35 +23,50 @@ import configparser import os -APP_ID = 'com.github.cascade.coremon' -CONFIG_DIR = os.path.expanduser('~/.config/coremon') -CONFIG_FILE = os.path.join(CONFIG_DIR, 'config.ini') +APP_ID = "com.github.cascade.coremon" +CONFIG_DIR = os.path.expanduser("~/.config/coremon") +CONFIG_FILE = os.path.join(CONFIG_DIR, "config.ini") + class CoreMonApp(Gtk.Application): def __init__(self): - super().__init__(application_id=APP_ID, - flags=Gio.ApplicationFlags.FLAGS_NONE) - + super().__init__(application_id=APP_ID, flags=Gio.ApplicationFlags.FLAGS_NONE) + self.start_minimized_arg = False + # Default settings self.settings = { - 'update_interval': 3, # seconds - 'temperature_unit': 'C', - 'start_minimized': False, - 'autostart': False, - 'cores_to_monitor': 'avg', # 'all', 'avg', or comma-separated list of core indices (default: avg) - 'window_width': 800, - 'window_height': 600, - 'temp_threshold': 100, # Temperature threshold for red coloring (Celsius) - 'load_threshold': 100, # Load threshold for red coloring (percentage) - 'show_individual_cores': False, # Whether to show individual core graphs (default: average only) - 'smooth_graphs': True, # Whether to use smooth lines in graphs - 'dashboard_avg_only': False # Whether to show only average on dashboard + "update_interval": 3, # seconds + "temperature_unit": "C", + "start_minimized": False, + "autostart": False, + "cores_to_monitor": "avg", # 'all', 'avg', or comma-separated list of core indices (default: avg) + "window_width": 800, + "window_height": 600, + "temp_threshold": 100, # Temperature threshold for red coloring (Celsius) + "load_threshold": 100, # Load threshold for red coloring (percentage) + "show_individual_cores": False, # Whether to show individual core graphs (default: average only) + "smooth_graphs": True, # Whether to use smooth lines in graphs + "dashboard_avg_only": False, # Whether to show only average on dashboard } - + self.load_settings() + self.ensure_default_config() # Ensure config file exists self.setup_data_structures() self.setup_indicator() - + + def do_command_line(self, command_line): + """Handle command line arguments""" + args = command_line.get_arguments() + + # Check for --minimized flag + if "--minimized" in args: + self.start_minimized_arg = True + print("Starting minimized due to --minimized flag") + + # Activate the application + self.activate() + return 0 + def setup_data_structures(self): self.max_data_points = 60 # Number of points to keep in history self.cpu_count = psutil.cpu_count() @@ -54,84 +74,189 @@ def setup_data_structures(self): self.temp_data = {} self.load_data = {} self.start_time = time.time() # Track application start time - + # Initialize data structures for each core for i in range(self.cpu_count): self.temp_data[i] = [] self.load_data[i] = [] - + def load_settings(self): if not os.path.exists(CONFIG_FILE): return - + config = configparser.ConfigParser() config.read(CONFIG_FILE) - - if 'CoreMon' in config: + + if "CoreMon" in config: for key in self.settings: - if key in config['CoreMon']: - if key in ['window_width', 'window_height', 'update_interval', 'temp_threshold', 'load_threshold']: - self.settings[key] = config['CoreMon'].getint(key, self.settings[key]) - elif key in ['start_minimized', 'autostart', 'show_individual_cores', 'smooth_graphs', 'dashboard_avg_only']: - self.settings[key] = config['CoreMon'].getboolean(key, self.settings[key]) + if key in config["CoreMon"]: + if key in [ + "window_width", + "window_height", + "update_interval", + "temp_threshold", + "load_threshold", + ]: + self.settings[key] = config["CoreMon"].getint( + key, self.settings[key] + ) + elif key in [ + "start_minimized", + "autostart", + "show_individual_cores", + "smooth_graphs", + "dashboard_avg_only", + ]: + self.settings[key] = config["CoreMon"].getboolean( + key, self.settings[key] + ) else: - self.settings[key] = config['CoreMon'].get(key, self.settings[key]) - + self.settings[key] = config["CoreMon"].get( + key, self.settings[key] + ) + def save_settings(self): os.makedirs(CONFIG_DIR, exist_ok=True) config = configparser.ConfigParser() - config['CoreMon'] = self.settings - - with open(CONFIG_FILE, 'w') as configfile: + config["CoreMon"] = self.settings + + with open(CONFIG_FILE, "w") as configfile: config.write(configfile) + configfile.flush() # Force write to disk + os.fsync(configfile.fileno()) # Ensure OS writes to disk + + def ensure_default_config(self): + """Ensure a default config file exists with proper settings""" + if not os.path.exists(CONFIG_FILE): + print("No configuration file found, creating default settings...") + self.save_settings() + print(f"Default configuration created at {CONFIG_FILE}") + + def setup_autostart(self, enable): + """Enable or disable autostart by creating/removing desktop file in ~/.config/autostart/""" + try: + autostart_dir = os.path.expanduser("~/.config/autostart") + autostart_file = os.path.join(autostart_dir, "coremon.desktop") + + if enable: + print("DEBUG: Setting up autostart...") + # Create autostart directory if it doesn't exist + os.makedirs(autostart_dir, exist_ok=True) + + # Get the path to the coremon executable + # Try to find it in common locations + possible_paths = [ + "/usr/bin/coremon", + "/usr/local/bin/coremon", + os.path.expanduser("~/bin/coremon"), + ] + + coremon_path = None + for path in possible_paths: + if os.path.isfile(path): + coremon_path = path + print(f"DEBUG: Found executable at {path}") + break + + # Fallback to python3 -m coremon if no executable found + if not coremon_path: + coremon_path = "python3 -m coremon" + print("DEBUG: Using fallback python3 -m coremon") + + # Build the exec command with --minimized flag if start_minimized is enabled + exec_command = coremon_path + if self.settings.get("start_minimized", False): + exec_command += " --minimized" + print(f"DEBUG: Adding --minimized flag, exec: {exec_command}") + + # Create desktop entry content + desktop_content = f"""[Desktop Entry] +Type=Application +Name=CoreMon +Comment=System monitoring tool +Exec={exec_command} +Icon=temperature +Terminal=false +Categories=System;Monitor; +X-GNOME-Autostart-enabled=true +""" + + # Write the autostart desktop file + print(f"DEBUG: Writing autostart file to {autostart_file}") + with open(autostart_file, "w") as f: + f.write(desktop_content) + + print(f"Autostart enabled: {autostart_file}") + + else: + print("DEBUG: Removing autostart...") + # Remove the autostart file if it exists + if os.path.exists(autostart_file): + print(f"DEBUG: Removing {autostart_file}") + os.remove(autostart_file) + print(f"Autostart disabled: removed {autostart_file}") + else: + print("DEBUG: Autostart file not found, nothing to remove") + + except Exception as e: + print(f"ERROR in setup_autostart: {e}") + # Don't crash the app - just log the error + # The setting will still be saved, but autostart might not work + print(f"WARNING: Autostart setup failed, but continuing...") def do_activate(self): # Show the window self.win = CoreMonWindow(self) self.win.show_all() - + # Initialize menu label based on initial visibility self.update_menu_label() + + # Check both settings and command line argument for start minimized + should_start_minimized = self.settings["start_minimized"] or self.start_minimized_arg - if self.settings['start_minimized']: + if should_start_minimized: + # Hide the window after it's shown for minimized start self.win.hide() + self.win.set_skip_taskbar_hint(True) self.show_hide_item.set_label("Show") - + # else: window remains shown normally + def setup_indicator(self): # Create system tray indicator with temperature icon self.indicator = AppIndicator3.Indicator.new( "coremon", "temperature", # Temperature icon - AppIndicator3.IndicatorCategory.APPLICATION_STATUS + AppIndicator3.IndicatorCategory.APPLICATION_STATUS, ) self.indicator.set_status(AppIndicator3.IndicatorStatus.ACTIVE) - + # Create menu self.menu = Gtk.Menu() - + # Show/Hide toggle item self.show_hide_item = Gtk.MenuItem(label="Hide") self.show_hide_item.connect("activate", self.on_show_hide) self.menu.append(self.show_hide_item) - + # Separator self.menu.append(Gtk.SeparatorMenuItem()) - + # Quit item quit_item = Gtk.MenuItem(label="Quit") quit_item.connect("activate", self.on_quit) self.menu.append(quit_item) - + self.menu.show_all() self.indicator.set_menu(self.menu) - + # Start updating indicator self.update_indicator() - + def on_show_hide(self, widget): - if not hasattr(self, 'win') or self.win is None: + if not hasattr(self, "win") or self.win is None: return - + if self.win.is_visible(): # Hide the window and remove from taskbar self.win.hide() @@ -142,70 +267,80 @@ def on_show_hide(self, widget): self.win.present() self.win.set_skip_taskbar_hint(False) self.show_hide_item.set_label("Hide") - + def update_menu_label(self): """Update the show/hide menu label based on window visibility""" - if not hasattr(self, 'win') or self.win is None: + if not hasattr(self, "win") or self.win is None: return - + if self.win.is_visible(): self.show_hide_item.set_label("Hide") else: self.show_hide_item.set_label("Show") - + def on_quit(self, widget): # Clean up timers before quitting - if hasattr(self, 'win') and hasattr(self.win, 'cleanup'): + if hasattr(self, "win") and hasattr(self.win, "cleanup"): self.win.cleanup() self.quit() - + def update_indicator(self): # Get current CPU data temps = self.get_cpu_temps() loads = self.get_cpu_loads() - + if temps and loads: # Use the same filtering logic as the main UI # If dashboard_avg_only is enabled, always return all cores for averaging - if self.settings.get('dashboard_avg_only', False): + if self.settings.get("dashboard_avg_only", False): filtered_temps = temps filtered_loads = loads - elif self.settings['cores_to_monitor'] == 'avg': + elif self.settings["cores_to_monitor"] == "avg": # Return all cores for averaging filtered_temps = temps filtered_loads = loads - elif self.settings['cores_to_monitor'] == 'all': + elif self.settings["cores_to_monitor"] == "all": filtered_temps = temps filtered_loads = loads else: # Return specific core - core_num = int(self.settings['cores_to_monitor']) - filtered_temps = {core_num: temps[core_num]} if core_num in temps else {} - filtered_loads = {core_num: loads[core_num]} if core_num in loads else {} - + core_num = int(self.settings["cores_to_monitor"]) + filtered_temps = ( + {core_num: temps[core_num]} if core_num in temps else {} + ) + filtered_loads = ( + {core_num: loads[core_num]} if core_num in loads else {} + ) + if filtered_temps and filtered_loads: avg_temp = sum(filtered_temps.values()) / len(filtered_temps) avg_load = sum(filtered_loads.values()) / len(filtered_loads) - + # Convert to Fahrenheit if needed temp_unit = "°C" - if self.settings['temperature_unit'] == 'F': - avg_temp = avg_temp * 9/5 + 32 + if self.settings["temperature_unit"] == "F": + avg_temp = avg_temp * 9 / 5 + 32 temp_unit = "°F" - temp_threshold = self.settings['temp_threshold'] * 9/5 + 32 + temp_threshold = self.settings["temp_threshold"] * 9 / 5 + 32 else: - temp_threshold = self.settings['temp_threshold'] - + temp_threshold = self.settings["temp_threshold"] + # Determine color based on thresholds temp_color = self.get_indicator_color(avg_temp, temp_threshold) - load_color = self.get_indicator_color(avg_load, self.settings['load_threshold']) - + load_color = self.get_indicator_color( + avg_load, self.settings["load_threshold"] + ) + # Update indicator label with simple text (no color coding in system tray) - self.indicator.set_label(f"{avg_temp:.0f}{temp_unit} {avg_load:.0f}%", "") - + self.indicator.set_label( + f"{avg_temp:.0f}{temp_unit} {avg_load:.0f}%", "" + ) + # Schedule next update - GLib.timeout_add_seconds(self.settings['update_interval'], self.update_indicator) - + GLib.timeout_add_seconds( + self.settings["update_interval"], self.update_indicator + ) + def get_indicator_color(self, value, threshold): """Get color indicator for system tray""" if value >= threshold: @@ -214,33 +349,83 @@ def get_indicator_color(self, value, threshold): return "orange" else: return "black" - + def get_cpu_temps(self): - """Get CPU temperatures for all cores""" + """Get CPU temperatures for all cores with universal sensor support""" temps = {} + detected_sensor = None + try: # Try to get temperatures using psutil - if hasattr(psutil, 'sensors_temperatures'): + if hasattr(psutil, "sensors_temperatures"): temps_info = psutil.sensors_temperatures() - if 'coretemp' in temps_info: - for entry in temps_info['coretemp']: - if 'Core' in entry.label: - # Extract core number from label (e.g., 'Core 0' -> 0) - try: - core_num = int(entry.label.split()[1]) - temps[core_num] = entry.current - except (IndexError, ValueError): - continue + + # Define sensor priority: Intel -> AMD -> ARM -> Generic + sensor_priority = [ + ("coretemp", "Core", "Intel"), # Intel CPUs + ("k10temp", ["Tctl", "Tdie", "Tccd"], "AMD"), # AMD CPUs + ("zenpower", ["Tctl", "Tdie", "Tccd"], "AMD"), # AMD Zen CPUs + ("cpu_thermal", "cpu-thermal", "ARM"), # ARM/Raspberry Pi + ] + + for sensor_name, label_patterns, sensor_type in sensor_priority: + if sensor_name in temps_info: + detected_sensor = sensor_type + print( + f"Detected {sensor_type} temperature sensor: {sensor_name}" + ) + + # Handle different label patterns for different sensor types + if isinstance(label_patterns, str): + # Single pattern (Intel, ARM) + for entry in temps_info[sensor_name]: + if label_patterns in entry.label: + # Extract core number for Intel + if sensor_type == "Intel": + try: + core_num = int(entry.label.split()[1]) + temps[core_num] = entry.current + except (IndexError, ValueError): + continue + else: + # For ARM, use sensor index as core number + temps[len(temps)] = entry.current + else: + # Multiple patterns (AMD) + for entry in temps_info[sensor_name]: + if any( + pattern in entry.label for pattern in label_patterns + ): + # Use index as core number for AMD + temps[len(temps)] = entry.current + + break # Stop after finding the first supported sensor + + # If no specific sensor found, try generic fallback + if not temps and temps_info: + print("No specific CPU sensor detected, trying generic fallback...") + for sensor_name, entries in temps_info.items(): + if entries and entries[0].current > 0: # Valid temperature + detected_sensor = "Generic" + print(f"Using generic sensor: {sensor_name}") + temps[0] = entries[0].current # Use first available temp + break + + if not temps: + print("Warning: No temperature sensors detected") + detected_sensor = "None" + except Exception as e: print(f"Error getting CPU temps: {e}") - - # Fallback to psutil if no temperatures found - if not temps and hasattr(psutil, 'sensors_temperatures'): - # Just return a dummy value for each core + detected_sensor = "Error" + + # Only use dummy fallback if we really couldn't get any temperatures + if not temps: + print("Using dummy temperature values - please check sensor support") temps = {i: 40.0 + i for i in range(self.cpu_count)} - + return temps - + def get_cpu_loads(self): """Get CPU load percentages for all cores""" loads = {} @@ -249,45 +434,48 @@ def get_cpu_loads(self): loads[i] = load return loads + class CoreMonWindow(Gtk.ApplicationWindow): def __init__(self, app): super().__init__(application=app) - + self.app = app self.set_default_size( - self.app.settings['window_width'], - self.app.settings['window_height'] + self.app.settings["window_width"], self.app.settings["window_height"] ) - + + # Track previous autostart state for async handling + self._previous_autostart_state = self.app.settings.get("autostart", False) + self.setup_ui() self.start_monitoring() - + # Connect delete event to hide window instead of closing self.connect("delete-event", self.on_delete_event) # Connect window state events to update menu self.connect("window-state-event", self.on_window_state_event) - + def on_delete_event(self, widget, event): # Hide window and remove from taskbar (same as Hide) self.hide() self.set_skip_taskbar_hint(True) self.app.show_hide_item.set_label("Show") return True # Prevent the default handler from running - + def on_window_state_event(self, widget, event): """Handle window state changes to update menu label""" # Update menu label when window state changes self.app.update_menu_label() return False - + def cleanup(self): """Clean up timers and resources""" - if hasattr(self, 'uptime_timer'): + if hasattr(self, "uptime_timer"): GLib.source_remove(self.uptime_timer) - + def setup_ui(self): self.set_title("CoreMon - System Monitor") - + # Apply CSS styling css_provider = Gtk.CssProvider() css_provider.load_from_data(""" @@ -322,9 +510,9 @@ def setup_ui(self): Gtk.StyleContext.add_provider_for_screen( Gdk.Screen.get_default(), css_provider, - Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION + Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION, ) - + # Main container with better spacing main_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12) main_box.set_margin_top(12) @@ -332,20 +520,20 @@ def setup_ui(self): main_box.set_margin_start(12) main_box.set_margin_end(12) self.add(main_box) - + # Create notebook for tabs notebook = Gtk.Notebook() main_box.pack_start(notebook, True, True, 0) - + # Dashboard tab dashboard_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=16) notebook.append_page(dashboard_box, Gtk.Label(label="Dashboard")) - + # Stats cards container stats_container = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=16) stats_container.set_homogeneous(True) dashboard_box.pack_start(stats_container, False, False, 0) - + # Temperature card with visible frame temp_card_frame = Gtk.Frame() temp_card_frame.set_shadow_type(Gtk.ShadowType.OUT) @@ -355,21 +543,21 @@ def setup_ui(self): temp_card.set_margin_start(12) temp_card.set_margin_end(12) temp_card_frame.add(temp_card) - + temp_icon = Gtk.Image.new_from_icon_name("temperature", Gtk.IconSize.DIALOG) temp_icon.get_style_context().add_class("stat-icon") temp_card.pack_start(temp_icon, False, False, 0) - + temp_label_title = Gtk.Label(label="Temperature") temp_label_title.get_style_context().add_class("stat-label") temp_card.pack_start(temp_label_title, False, False, 0) - + self.temp_label = Gtk.Label(label="N/A°C") self.temp_label.get_style_context().add_class("stat-value") temp_card.pack_start(self.temp_label, False, False, 0) - + stats_container.pack_start(temp_card_frame, True, True, 0) - + # CPU Load card with visible frame load_card_frame = Gtk.Frame() load_card_frame.set_shadow_type(Gtk.ShadowType.OUT) @@ -379,256 +567,278 @@ def setup_ui(self): load_card.set_margin_start(12) load_card.set_margin_end(12) load_card_frame.add(load_card) - + load_icon = Gtk.Image.new_from_icon_name("system-run", Gtk.IconSize.DIALOG) load_icon.set_pixel_size(48) # Force size to match temperature icon load_icon.get_style_context().add_class("stat-icon") load_card.pack_start(load_icon, False, False, 0) - + load_label_title = Gtk.Label(label="CPU Load") load_label_title.get_style_context().add_class("stat-label") load_card.pack_start(load_label_title, False, False, 0) - + self.load_label = Gtk.Label(label="N/A%") self.load_label.get_style_context().add_class("stat-value") load_card.pack_start(self.load_label, False, False, 0) - + stats_container.pack_start(load_card_frame, True, True, 0) - + # Graphs section graph_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8) dashboard_box.pack_start(graph_box, True, True, 0) - + # Temperature graph (no header, has title in graph) - self.temp_fig = Figure(figsize=(6, 2.5), dpi=100, facecolor='#2b2b2b') - self.temp_ax = self.temp_fig.add_subplot(111, facecolor='#2b2b2b') - self.temp_line, = self.temp_ax.plot([], [], 'r-') - self.temp_ax.set_ylabel('Temperature (°C)') + self.temp_fig = Figure(figsize=(6, 2.5), dpi=100, facecolor="#2b2b2b") + self.temp_ax = self.temp_fig.add_subplot(111, facecolor="#2b2b2b") + (self.temp_line,) = self.temp_ax.plot([], [], "r-") + self.temp_ax.set_ylabel("Temperature (°C)") self.temp_ax.grid(True) - + self.temp_canvas = FigureCanvas(self.temp_fig) graph_box.pack_start(self.temp_canvas, True, True, 0) - + # Load graph (no header, has title in graph) - self.load_fig = Figure(figsize=(6, 2.5), dpi=100, facecolor='#2b2b2b') - self.load_ax = self.load_fig.add_subplot(111, facecolor='#2b2b2b') - self.load_line, = self.load_ax.plot([], [], 'b-') - self.load_ax.set_ylabel('Load (%)') + self.load_fig = Figure(figsize=(6, 2.5), dpi=100, facecolor="#2b2b2b") + self.load_ax = self.load_fig.add_subplot(111, facecolor="#2b2b2b") + (self.load_line,) = self.load_ax.plot([], [], "b-") + self.load_ax.set_ylabel("Load (%)") self.load_ax.set_ylim(0, 100) # 0-100% range for CPU load self.load_ax.grid(True) - + self.load_canvas = FigureCanvas(self.load_fig) graph_box.pack_start(self.load_canvas, True, True, 0) - + # Settings tab - settings_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12, margin=12) + settings_box = Gtk.Box( + orientation=Gtk.Orientation.VERTICAL, spacing=12, margin=12 + ) notebook.append_page(settings_box, Gtk.Label(label="Settings")) - + # Add settings controls here settings_label = Gtk.Label(label="CoreMon Settings") settings_label.get_style_context().add_class("title-2") settings_box.pack_start(settings_label, False, False, 0) - + # Update interval interval_box = Gtk.Box(spacing=12) interval_label = Gtk.Label(label="Update interval (seconds):") interval_box.pack_start(interval_label, False, False, 0) - + self.interval_spin = Gtk.SpinButton.new_with_range(1, 60, 1) - self.interval_spin.set_value(self.app.settings['update_interval']) + self.interval_spin.set_value(self.app.settings["update_interval"]) self.interval_spin.connect("value-changed", self.on_setting_changed) interval_box.pack_start(self.interval_spin, False, False, 0) - + settings_box.pack_start(interval_box, False, False, 0) - + # Temperature unit unit_box = Gtk.Box(spacing=12) unit_label = Gtk.Label(label="Temperature unit:") unit_box.pack_start(unit_label, False, False, 0) - + self.unit_combo = Gtk.ComboBoxText() self.unit_combo.append("C", "Celsius (°C)") self.unit_combo.append("F", "Fahrenheit (°F)") - self.unit_combo.set_active_id(self.app.settings['temperature_unit']) + self.unit_combo.set_active_id(self.app.settings["temperature_unit"]) self.unit_combo.connect("changed", self.on_setting_changed) unit_box.pack_start(self.unit_combo, False, False, 0) - + settings_box.pack_start(unit_box, False, False, 0) - + # Temperature threshold temp_threshold_box = Gtk.Box(spacing=12) temp_threshold_label = Gtk.Label(label="Temperature threshold (°C):") temp_threshold_box.pack_start(temp_threshold_label, False, False, 0) - + self.temp_threshold_spin = Gtk.SpinButton.new_with_range(50, 150, 5) - self.temp_threshold_spin.set_value(self.app.settings['temp_threshold']) + self.temp_threshold_spin.set_value(self.app.settings["temp_threshold"]) self.temp_threshold_spin.connect("value-changed", self.on_setting_changed) temp_threshold_box.pack_start(self.temp_threshold_spin, False, False, 0) - + settings_box.pack_start(temp_threshold_box, False, False, 0) - + # Load threshold load_threshold_box = Gtk.Box(spacing=12) load_threshold_label = Gtk.Label(label="Load threshold (%):") load_threshold_box.pack_start(load_threshold_label, False, False, 0) - + self.load_threshold_spin = Gtk.SpinButton.new_with_range(50, 100, 5) - self.load_threshold_spin.set_value(self.app.settings['load_threshold']) + self.load_threshold_spin.set_value(self.app.settings["load_threshold"]) self.load_threshold_spin.connect("value-changed", self.on_setting_changed) load_threshold_box.pack_start(self.load_threshold_spin, False, False, 0) - + settings_box.pack_start(load_threshold_box, False, False, 0) - + # Core selection core_selection_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) core_selection_label = Gtk.Label(label="Core monitoring:") core_selection_label.set_halign(Gtk.Align.START) core_selection_box.pack_start(core_selection_label, False, False, 0) - + self.core_combo = Gtk.ComboBoxText() self.core_combo.append("all", "All cores") self.core_combo.append("avg", "Average only") for i in range(self.app.cpu_count): self.core_combo.append(str(i), f"Core {i}") - self.core_combo.set_active_id(self.app.settings['cores_to_monitor']) + self.core_combo.set_active_id(self.app.settings["cores_to_monitor"]) self.core_combo.connect("changed", self.on_setting_changed) core_selection_box.pack_start(self.core_combo, False, False, 0) - + settings_box.pack_start(core_selection_box, False, False, 0) - + # Show individual cores self.show_individual_cores_switch = Gtk.Switch() - self.show_individual_cores_switch.set_active(self.app.settings['show_individual_cores']) - self.show_individual_cores_switch.connect("notify::active", self.on_setting_changed) + self.show_individual_cores_switch.set_active( + self.app.settings["show_individual_cores"] + ) + self.show_individual_cores_switch.connect( + "notify::active", self.on_setting_changed + ) individual_cores_box = Gtk.Box(spacing=12) individual_cores_label = Gtk.Label(label="Show individual core graphs:") individual_cores_box.pack_start(individual_cores_label, False, False, 0) - individual_cores_box.pack_end(self.show_individual_cores_switch, False, False, 0) + individual_cores_box.pack_end( + self.show_individual_cores_switch, False, False, 0 + ) settings_box.pack_start(individual_cores_box, False, False, 0) - + # Smooth graphs self.smooth_graphs_switch = Gtk.Switch() - self.smooth_graphs_switch.set_active(self.app.settings['smooth_graphs']) + self.smooth_graphs_switch.set_active(self.app.settings["smooth_graphs"]) self.smooth_graphs_switch.connect("notify::active", self.on_setting_changed) smooth_graphs_box = Gtk.Box(spacing=12) smooth_graphs_label = Gtk.Label(label="Smooth graph lines:") smooth_graphs_box.pack_start(smooth_graphs_label, False, False, 0) smooth_graphs_box.pack_end(self.smooth_graphs_switch, False, False, 0) settings_box.pack_start(smooth_graphs_box, False, False, 0) - + # Dashboard average only self.dashboard_avg_only_switch = Gtk.Switch() - self.dashboard_avg_only_switch.set_active(self.app.settings['dashboard_avg_only']) - self.dashboard_avg_only_switch.connect("notify::active", self.on_setting_changed) + self.dashboard_avg_only_switch.set_active( + self.app.settings["dashboard_avg_only"] + ) + self.dashboard_avg_only_switch.connect( + "notify::active", self.on_setting_changed + ) dashboard_avg_only_box = Gtk.Box(spacing=12) dashboard_avg_only_label = Gtk.Label(label="Dashboard: Show average only:") dashboard_avg_only_box.pack_start(dashboard_avg_only_label, False, False, 0) dashboard_avg_only_box.pack_end(self.dashboard_avg_only_switch, False, False, 0) settings_box.pack_start(dashboard_avg_only_box, False, False, 0) - + # Start minimized self.start_minimized_switch = Gtk.Switch() - self.start_minimized_switch.set_active(self.app.settings['start_minimized']) + self.start_minimized_switch.set_active(self.app.settings["start_minimized"]) + self.start_minimized_switch.connect("notify::active", self.on_setting_changed) minimized_box = Gtk.Box(spacing=12) minimized_label = Gtk.Label(label="Start minimized:") minimized_box.pack_start(minimized_label, False, False, 0) minimized_box.pack_end(self.start_minimized_switch, False, False, 0) settings_box.pack_start(minimized_box, False, False, 0) - + # Autostart self.autostart_switch = Gtk.Switch() - self.autostart_switch.set_active(self.app.settings['autostart']) + self.autostart_switch.set_active(self.app.settings["autostart"]) + self.autostart_switch.connect("notify::active", self.on_setting_changed) autostart_box = Gtk.Box(spacing=12) autostart_label = Gtk.Label(label="Start on login:") autostart_box.pack_start(autostart_label, False, False, 0) autostart_box.pack_end(self.autostart_switch, False, False, 0) settings_box.pack_start(autostart_box, False, False, 0) - - # Save button - save_button = Gtk.Button(label="Save Settings") - save_button.connect("clicked", self.on_save_settings) - settings_box.pack_end(save_button, False, False, 0) - + # System Info tab - system_info_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12, margin=12) + system_info_box = Gtk.Box( + orientation=Gtk.Orientation.VERTICAL, spacing=12, margin=12 + ) notebook.append_page(system_info_box, Gtk.Label(label="System Info")) - + # Main container for system info main_container = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12) system_info_box.pack_start(main_container, True, True, 0) - + # Left side - Neofetch output (no scrolling) left_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) main_container.pack_start(left_box, True, True, 0) - + # Create text view for system info without scrolling self.system_info_text = Gtk.TextView() self.system_info_text.set_editable(False) self.system_info_text.set_wrap_mode(Gtk.WrapMode.NONE) self.system_info_text.set_size_request(-1, 400) # Fixed height to fit window left_box.pack_start(self.system_info_text, True, True, 0) - + # Right side - Live uptime display right_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) main_container.pack_start(right_box, False, False, 0) - + # Uptime frame uptime_frame = Gtk.Frame(label="Live Uptime") uptime_frame.set_size_request(160, -1) # Fixed width for the entire frame uptime_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6, margin=12) uptime_frame.add(uptime_box) - + # System uptime system_uptime_label = Gtk.Label(label="System: 00:00:00") system_uptime_label.get_style_context().add_class("uptime-label") system_uptime_label.set_size_request(140, -1) # Fixed width to prevent movement system_uptime_label.set_halign(Gtk.Align.START) # Left align # Use monospace font for consistent character width - system_uptime_label.override_font(Pango.FontDescription.from_string("monospace 10")) + system_uptime_label.override_font( + Pango.FontDescription.from_string("monospace 10") + ) uptime_box.pack_start(system_uptime_label, False, False, 0) - + # CoreMon uptime coremon_uptime_label = Gtk.Label(label="CoreMon: 00:00:00") coremon_uptime_label.get_style_context().add_class("uptime-label") - coremon_uptime_label.set_size_request(140, -1) # Fixed width to prevent movement + coremon_uptime_label.set_size_request( + 140, -1 + ) # Fixed width to prevent movement coremon_uptime_label.set_halign(Gtk.Align.START) # Left align # Use monospace font for consistent character width - coremon_uptime_label.override_font(Pango.FontDescription.from_string("monospace 10")) + coremon_uptime_label.override_font( + Pango.FontDescription.from_string("monospace 10") + ) uptime_box.pack_start(coremon_uptime_label, False, False, 0) - + right_box.pack_start(uptime_frame, False, False, 0) - + # Store references for updates self.system_uptime_label = system_uptime_label self.coremon_uptime_label = coremon_uptime_label - + # Add refresh button refresh_button = Gtk.Button(label="Refresh System Info") refresh_button.connect("clicked", self.on_refresh_system_info) left_box.pack_start(refresh_button, False, False, 0) - + # Load initial system info self.update_system_info() - + # Start uptime refresh timer self.uptime_timer = GLib.timeout_add_seconds(1, self.refresh_uptime_only) - + # About tab about_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12, margin=12) notebook.append_page(about_box, Gtk.Label(label="About")) - + about_label = Gtk.Label() + # Get current date and time for build info + from datetime import datetime + build_datetime = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + about_label.set_markup( - "CoreMon v1.0\n\n" + f"CoreMon v1.0.2\n\n" + f"Built: {build_datetime}\n\n" "A simple temperature and load monitor for Ubuntu based OS.\n" "Designed to give you a quick view of your temps and load.\n\n" "© 2025 CoreMon Project" ) about_label.set_justify(Gtk.Justification.CENTER) about_box.pack_start(about_label, True, True, 0) - + self.show_all() - + def refresh_uptime_only(self): """Refresh only the uptime labels every second""" try: @@ -636,37 +846,41 @@ def refresh_uptime_only(self): boot_time = psutil.boot_time() system_uptime = time.time() - boot_time coremon_uptime = time.time() - self.app.start_time - - self.system_uptime_label.set_text(f"System: {self.format_uptime(system_uptime)}") - self.coremon_uptime_label.set_text(f"CoreMon: {self.format_uptime(coremon_uptime)}") - + + self.system_uptime_label.set_text( + f"System: {self.format_uptime(system_uptime)}" + ) + self.coremon_uptime_label.set_text( + f"CoreMon: {self.format_uptime(coremon_uptime)}" + ) + except Exception as e: print(f"Error refreshing uptime: {e}") - + # Return True to keep the timer running return True - + def on_refresh_system_info(self, widget): """Refresh system information display""" self.update_system_info() - + def update_system_info(self): """Update system information display""" try: # Get system information info_text = self.get_system_info() - + # Update text view text_buffer = self.system_info_text.get_buffer() text_buffer.set_text(info_text) - + # Apply monospace font for better formatting self.system_info_text.override_font( Pango.FontDescription.from_string("monospace 10") ) except Exception as e: print(f"Error updating system info: {e}") - + def get_system_info(self): """Get actual neofetch output with minimal additions""" import subprocess @@ -676,15 +890,20 @@ def get_system_info(self): # Try to get actual neofetch output try: # Run neofetch and capture its output directly - result = subprocess.run(['neofetch', '--stdout'], - capture_output=True, text=True, timeout=10) + result = subprocess.run( + ["neofetch", "--stdout"], capture_output=True, text=True, timeout=10 + ) if result.returncode == 0 and result.stdout.strip(): # Add neofetch output directly without headers info_lines.append(result.stdout.strip()) else: # Fallback if neofetch fails info_lines.append("! Neofetch output unavailable") - except (subprocess.TimeoutExpired, FileNotFoundError, subprocess.SubprocessError) as e: + except ( + subprocess.TimeoutExpired, + FileNotFoundError, + subprocess.SubprocessError, + ) as e: info_lines.append("! Neofetch not installed or not working") info_lines.append(f"! Install with: sudo apt install neofetch") @@ -693,16 +912,20 @@ def get_system_info(self): info_lines.append("PROCESS INFORMATION") info_lines.append("-" * 30) info_lines.append(f"Total processes: {len(psutil.pids())}") - info_lines.append(f"Running: {sum(1 for p in psutil.process_iter(['status']) if p.info['status'] == psutil.STATUS_RUNNING)}") - info_lines.append(f"Sleeping: {sum(1 for p in psutil.process_iter(['status']) if p.info['status'] == psutil.STATUS_SLEEPING)}") + info_lines.append( + f"Running: {sum(1 for p in psutil.process_iter(['status']) if p.info['status'] == psutil.STATUS_RUNNING)}" + ) + info_lines.append( + f"Sleeping: {sum(1 for p in psutil.process_iter(['status']) if p.info['status'] == psutil.STATUS_SLEEPING)}" + ) return "\n".join(info_lines) - + def create_usage_bar(self, percentage, width=20): """Create a visual usage bar""" filled = int(width * percentage / 100) bar = "█" * filled + "░" * (width - filled) - + # Color coding based on percentage if percentage >= 90: return f"[{bar}]" @@ -712,13 +935,13 @@ def create_usage_bar(self, percentage, width=20): return f"[{bar}]" else: return f"[{bar}]" - + def create_temp_bar(self, temp, max_temp=100, width=20): """Create a visual temperature bar""" percentage = min(temp / max_temp * 100, 100) filled = int(width * percentage / 100) bar = "█" * filled + "░" * (width - filled) - + # Color coding based on temperature if temp >= 80: return f"[{bar}]" @@ -728,33 +951,33 @@ def create_temp_bar(self, temp, max_temp=100, width=20): return f"[{bar}]" else: return f"[{bar}]" - + def get_cpu_info(self): """Get CPU model information""" try: - with open('/proc/cpuinfo', 'r') as f: + with open("/proc/cpuinfo", "r") as f: for line in f: - if 'model name' in line: - return line.split(':')[1].strip() + if "model name" in line: + return line.split(":")[1].strip() except: return "Unknown CPU" - + def get_memory_info(self): """Get formatted memory information""" memory = psutil.virtual_memory() return f"{self.format_bytes(memory.total)} ({memory.percent:.1f}% used)" - + def get_disk_info(self): """Get disk usage information""" try: - disk = psutil.disk_usage('/') + disk = psutil.disk_usage("/") return f"{self.format_bytes(disk.total)} ({disk.percent:.1f}% used)" except: return "Unknown" - + def format_bytes(self, bytes_value): """Format bytes to human readable format""" - for unit in ['B', 'KB', 'MB', 'GB', 'TB']: + for unit in ["B", "KB", "MB", "GB", "TB"]: if bytes_value < 1024.0: return f"{bytes_value:.2f} {unit}" bytes_value /= 1024.0 @@ -766,157 +989,218 @@ def format_uptime(self, seconds): hours = int((seconds % 86400) // 3600) minutes = int((seconds % 3600) // 60) secs = int(seconds % 60) - + if days > 0: return f"{days:02d}:{hours:02d}:{minutes:02d}:{secs:02d}" else: return f"{hours:02d}:{minutes:02d}:{secs:02d}" def on_setting_changed(self, widget): - # Apply settings immediately - self.app.settings['update_interval'] = self.interval_spin.get_value_as_int() - self.app.settings['temperature_unit'] = self.unit_combo.get_active_id() - self.app.settings['temp_threshold'] = self.temp_threshold_spin.get_value_as_int() - self.app.settings['load_threshold'] = self.load_threshold_spin.get_value_as_int() - self.app.settings['cores_to_monitor'] = self.core_combo.get_active_id() - self.app.settings['show_individual_cores'] = self.show_individual_cores_switch.get_active() - self.app.settings['smooth_graphs'] = self.smooth_graphs_switch.get_active() - self.app.settings['dashboard_avg_only'] = self.dashboard_avg_only_switch.get_active() - self.app.settings['start_minimized'] = self.start_minimized_switch.get_active() - self.app.settings['autostart'] = self.autostart_switch.get_active() + try: + print(f"DEBUG: on_setting_changed called with widget: {type(widget)}") + + # Store the OLD autostart state BEFORE updating settings + old_autostart_state = self.app.settings.get("autostart", False) + + # Apply settings immediately + self.app.settings["update_interval"] = self.interval_spin.get_value_as_int() + self.app.settings["temperature_unit"] = self.unit_combo.get_active_id() + self.app.settings["temp_threshold"] = ( + self.temp_threshold_spin.get_value_as_int() + ) + self.app.settings["load_threshold"] = ( + self.load_threshold_spin.get_value_as_int() + ) + self.app.settings["cores_to_monitor"] = self.core_combo.get_active_id() + self.app.settings["show_individual_cores"] = ( + self.show_individual_cores_switch.get_active() + ) + self.app.settings["smooth_graphs"] = self.smooth_graphs_switch.get_active() + self.app.settings["dashboard_avg_only"] = ( + self.dashboard_avg_only_switch.get_active() + ) + self.app.settings["start_minimized"] = self.start_minimized_switch.get_active() + + print(f"DEBUG: Setting autostart from switch: {self.autostart_switch.get_active()}") + self.app.settings["autostart"] = self.autostart_switch.get_active() + + print(f"DEBUG: Current settings - start_minimized: {self.app.settings['start_minimized']}, autostart: {self.app.settings['autostart']}") + + # Save to config file + self.app.save_settings() + print("DEBUG: Settings saved successfully") + + # Handle autostart setting change in background thread to prevent UI blocking + if self.app.settings["autostart"] != old_autostart_state: + print(f"DEBUG: Autostart state changed from {old_autostart_state} to {self.app.settings['autostart']}") + self._handle_autostart_change_async() + else: + print("DEBUG: Autostart state unchanged, skipping async handling") + + # Update tracking variable AFTER the comparison + self._previous_autostart_state = self.app.settings["autostart"] + + # Clear data to force refresh with new settings + self.app.time_data.clear() + for i in range(self.app.cpu_count): + self.app.temp_data[i].clear() + self.app.load_data[i].clear() + + print("DEBUG: on_setting_changed completed successfully") + + except Exception as e: + print(f"ERROR in on_setting_changed: {e}") + print(f"ERROR traceback: {traceback.format_exc()}") + # Don't crash the app - just log the error + print("WARNING: Settings change failed, but continuing...") + + def start_monitoring(self): + self.update_ui() + + def _handle_autostart_change_async(self): + """Handle autostart changes in background thread to prevent UI blocking""" + from gi.repository import GLib - # Save to config file - self.app.save_settings() + def autostart_worker(): + try: + print(f"DEBUG: Starting background autostart setup for {self.app.settings['autostart']}") + + # Give immediate UI feedback + GLib.idle_add(self._show_autostart_feedback, "Setting up autostart...") + + # Call the actual setup method + self.app.setup_autostart(self.app.settings["autostart"]) + + # Update previous state + self._previous_autostart_state = self.app.settings["autostart"] + + # Give completion feedback + GLib.idle_add(self._show_autostart_feedback, "Autostart setup complete") + GLib.timeout_add(2000, self._hide_autostart_feedback) # Hide after 2 seconds + + print("DEBUG: Background autostart setup complete") + + except Exception as e: + print(f"ERROR in background autostart: {e}") + GLib.idle_add(self._show_autostart_feedback, f"Autostart error: {str(e)}") + GLib.timeout_add(3000, self._hide_autostart_feedback) + self._previous_autostart_state = not self.app.settings["autostart"] # Revert state + + return False # Stop the timeout - # Clear data to force refresh with new settings - self.app.time_data.clear() - for i in range(self.app.cpu_count): - self.app.temp_data[i].clear() - self.app.load_data[i].clear() + # Start the background thread + GLib.timeout_add(50, autostart_worker) # Small delay to let UI update first - def on_save_settings(self, widget): - # Save window size - width, height = self.get_size() - self.app.settings['window_width'] = width - self.app.settings['window_height'] = height - - # Save to config file - self.app.save_settings() - - # Show confirmation - dialog = Gtk.MessageDialog( - parent=self, - flags=0, - message_type=Gtk.MessageType.INFO, - buttons=Gtk.ButtonsType.OK, - text="Settings saved" - ) - dialog.format_secondary_text("Settings are applied immediately and will persist after restarting CoreMon.") - dialog.run() - dialog.destroy() + def _show_autostart_feedback(self, message): + """Show immediate feedback to user during autostart operations""" + # For now, just print to console - you could add a status label later + print(f"AUTOSTART: {message}") + # You could also update a status label or show a notification here + return False # Stop idle callback - def start_monitoring(self): - self.update_ui() - + def _hide_autostart_feedback(self): + """Hide autostart feedback""" + print("AUTOSTART: Operation complete") + return False # Stop timeout callback + def update_ui(self): # Get current CPU data temps = self.app.get_cpu_temps() loads = self.app.get_cpu_loads() - + # Update current stats if temps and loads: # Filter cores based on setting filtered_temps = self.filter_cores(temps) filtered_loads = self.filter_cores(loads) - + if filtered_temps: avg_temp = sum(filtered_temps.values()) / len(filtered_temps) - + # Convert to Fahrenheit if needed - if self.app.settings['temperature_unit'] == 'F': - avg_temp = avg_temp * 9/5 + 32 + if self.app.settings["temperature_unit"] == "F": + avg_temp = avg_temp * 9 / 5 + 32 temp_unit = "°F" # Convert threshold to Fahrenheit for comparison - temp_threshold = self.app.settings['temp_threshold'] * 9/5 + 32 + temp_threshold = self.app.settings["temp_threshold"] * 9 / 5 + 32 else: temp_unit = "°C" - temp_threshold = self.app.settings['temp_threshold'] - + temp_threshold = self.app.settings["temp_threshold"] + # Apply color coding using CSS classes temp_color_class = self.get_temp_color_class(avg_temp, temp_threshold) self.temp_label.set_text(f"{avg_temp:.1f}{temp_unit}") - + # Remove old color classes and add new one style_context = self.temp_label.get_style_context() for css_class in style_context.list_classes(): - if css_class.startswith('temp-'): + if css_class.startswith("temp-"): style_context.remove_class(css_class) style_context.add_class(temp_color_class) - + # Add to history elapsed_time = time.time() - self.app.start_time self.app.time_data.append(elapsed_time) - + # Keep only the last N data points if len(self.app.time_data) > self.app.max_data_points: self.app.time_data.pop(0) - + # Update temperature data for core, temp in filtered_temps.items(): # Convert to Fahrenheit if needed for storage display_temp = temp - if self.app.settings['temperature_unit'] == 'F': - display_temp = temp * 9/5 + 32 + if self.app.settings["temperature_unit"] == "F": + display_temp = temp * 9 / 5 + 32 self.app.temp_data[core].append(display_temp) if len(self.app.temp_data[core]) > self.app.max_data_points: self.app.temp_data[core].pop(0) - + # Update load data for core, load in filtered_loads.items(): self.app.load_data[core].append(load) if len(self.app.load_data[core]) > self.app.max_data_points: self.app.load_data[core].pop(0) - + # Update graphs self.update_graphs() - + if filtered_loads: avg_load = sum(filtered_loads.values()) / len(filtered_loads) - + # Apply color coding using CSS classes - load_color_class = self.get_load_color_class(avg_load, self.app.settings['load_threshold']) + load_color_class = self.get_load_color_class( + avg_load, self.app.settings["load_threshold"] + ) self.load_label.set_text(f"{avg_load:.1f}%") - + # Remove old color classes and add new one style_context = self.load_label.get_style_context() for css_class in style_context.list_classes(): - if css_class.startswith('load-'): + if css_class.startswith("load-"): style_context.remove_class(css_class) style_context.add_class(load_color_class) - + # Schedule next update - GLib.timeout_add_seconds( - self.app.settings['update_interval'], - self.update_ui - ) - + GLib.timeout_add_seconds(self.app.settings["update_interval"], self.update_ui) + def filter_cores(self, data): """Filter data based on core selection setting""" # If dashboard_avg_only is enabled, always return all cores for averaging - if self.app.settings['dashboard_avg_only']: + if self.app.settings["dashboard_avg_only"]: return data - elif self.app.settings['cores_to_monitor'] == 'avg': + elif self.app.settings["cores_to_monitor"] == "avg": # Return all cores for averaging (will be averaged in update_ui) return data - elif self.app.settings['cores_to_monitor'] == 'all': + elif self.app.settings["cores_to_monitor"] == "all": return data else: # Return specific core - core_num = int(self.app.settings['cores_to_monitor']) + core_num = int(self.app.settings["cores_to_monitor"]) if core_num in data: return {core_num: data[core_num]} return {} - + def get_temp_color_class(self, temp, threshold): """Get CSS color class for temperature based on threshold""" if temp >= threshold: @@ -927,7 +1211,7 @@ def get_temp_color_class(self, temp, threshold): return "temp-warm" else: return "temp-normal" - + def get_load_color_class(self, load, threshold): """Get CSS color class for load based on threshold""" if load >= threshold: @@ -938,7 +1222,7 @@ def get_load_color_class(self, load, threshold): return "load-warm" else: return "load-normal" - + def get_temp_color(self, temp, threshold): """Get color for temperature based on threshold - green/yellow/red""" if temp >= threshold: @@ -949,7 +1233,7 @@ def get_temp_color(self, temp, threshold): return "goldenrod" else: return "green" - + def get_load_color(self, load, threshold): """Get color for load based on threshold""" if load >= threshold: @@ -960,32 +1244,32 @@ def get_load_color(self, load, threshold): return "goldenrod" else: return "black" - + def update_graphs(self): if not self.app.time_data: return - + # Determine which cores to show based on settings - if not self.app.settings['show_individual_cores']: + if not self.app.settings["show_individual_cores"]: # Default: show only average line cores_to_show = [-1] # Use -1 for average - elif self.app.settings['cores_to_monitor'] == 'avg': + elif self.app.settings["cores_to_monitor"] == "avg": # Show only average line cores_to_show = [-1] # Use -1 for average - elif self.app.settings['cores_to_monitor'] == 'all': + elif self.app.settings["cores_to_monitor"] == "all": # Show all cores (when individual cores enabled) cores_to_show = list(range(self.app.cpu_count)) else: # Show specific core - cores_to_show = [int(self.app.settings['cores_to_monitor'])] - + cores_to_show = [int(self.app.settings["cores_to_monitor"])] + # Format time data for display time_labels = [self.format_elapsed_time(t) for t in self.app.time_data] - + # Update temperature graph self.temp_ax.clear() - self.temp_ax.set_facecolor('#2b2b2b') - + self.temp_ax.set_facecolor("#2b2b2b") + if -1 in cores_to_show: # Calculate and show average avg_temps = [] @@ -998,23 +1282,23 @@ def update_graphs(self): avg_temps.append(sum(temps_at_time) / len(temps_at_time)) else: avg_temps.append(0) - + # Determine line style based on smooth setting - line_style = '-' if self.app.settings['smooth_graphs'] else 'steps' - + line_style = "-" if self.app.settings["smooth_graphs"] else "steps" + self.temp_ax.plot( range(len(avg_temps)), avg_temps, label="Average", - color='red', + color="red", linewidth=2, alpha=0.8, - linestyle=line_style + linestyle=line_style, ) - + # Show individual cores if enabled in settings - if self.app.settings['show_individual_cores'] and -1 not in cores_to_show: - line_style = '-' if self.app.settings['smooth_graphs'] else 'steps' + if self.app.settings["show_individual_cores"] and -1 not in cores_to_show: + line_style = "-" if self.app.settings["smooth_graphs"] else "steps" for core in cores_to_show: if core in self.app.temp_data and self.app.temp_data[core]: self.temp_ax.plot( @@ -1022,41 +1306,45 @@ def update_graphs(self): self.app.temp_data[core], label=f"Core {core}", alpha=0.7, - linestyle=line_style + linestyle=line_style, ) - - temp_unit = "°C" if self.app.settings['temperature_unit'] == 'C' else "°F" - self.temp_ax.set_ylabel(f'Temperature ({temp_unit})', color='#cccccc') - self.temp_ax.set_title('CPU Temperature Over Time', color='#cccccc') - self.temp_ax.grid(True, alpha=0.3, color='#555555') - self.temp_ax.tick_params(colors='#cccccc') - + + temp_unit = "°C" if self.app.settings["temperature_unit"] == "C" else "°F" + self.temp_ax.set_ylabel(f"Temperature ({temp_unit})", color="#cccccc") + self.temp_ax.set_title("CPU Temperature Over Time", color="#cccccc") + self.temp_ax.grid(True, alpha=0.3, color="#555555") + self.temp_ax.tick_params(colors="#cccccc") + # Set x-axis labels to show elapsed time if len(time_labels) > 0: # Show every nth label to avoid crowding step = max(1, len(time_labels) // 10) self.temp_ax.set_xticks(range(0, len(time_labels), step)) - self.temp_ax.set_xticklabels([time_labels[i] for i in range(0, len(time_labels), step)]) - + self.temp_ax.set_xticklabels( + [time_labels[i] for i in range(0, len(time_labels), step)] + ) + # Anchor legend outside plot area (bottom) if cores_to_show or -1 in cores_to_show: - legend = self.temp_ax.legend(loc='upper center', bbox_to_anchor=(0.5, -0.25), ncol=4, framealpha=0.9) - legend.get_frame().set_facecolor('#2b2b2b') + legend = self.temp_ax.legend( + loc="upper center", bbox_to_anchor=(0.5, -0.25), ncol=4, framealpha=0.9 + ) + legend.get_frame().set_facecolor("#2b2b2b") for text in legend.get_texts(): - text.set_color('#cccccc') - + text.set_color("#cccccc") + # Rotate x-axis labels for better readability for label in self.temp_ax.get_xticklabels(): label.set_rotation(45) - label.set_ha('right') - + label.set_ha("right") + self.temp_fig.tight_layout(pad=1.5) self.temp_canvas.draw() - + # Update load graph self.load_ax.clear() - self.load_ax.set_facecolor('#2b2b2b') - + self.load_ax.set_facecolor("#2b2b2b") + if -1 in cores_to_show: # Calculate and show average avg_loads = [] @@ -1069,23 +1357,23 @@ def update_graphs(self): avg_loads.append(sum(loads_at_time) / len(loads_at_time)) else: avg_loads.append(0) - + # Determine line style based on smooth setting - line_style = '-' if self.app.settings['smooth_graphs'] else 'steps' - + line_style = "-" if self.app.settings["smooth_graphs"] else "steps" + self.load_ax.plot( range(len(avg_loads)), avg_loads, label="Average", - color='blue', + color="blue", linewidth=2, alpha=0.8, - linestyle=line_style + linestyle=line_style, ) - + # Show individual cores if enabled in settings - if self.app.settings['show_individual_cores'] and -1 not in cores_to_show: - line_style = '-' if self.app.settings['smooth_graphs'] else 'steps' + if self.app.settings["show_individual_cores"] and -1 not in cores_to_show: + line_style = "-" if self.app.settings["smooth_graphs"] else "steps" for core in cores_to_show: if core in self.app.load_data and self.app.load_data[core]: self.load_ax.plot( @@ -1093,37 +1381,41 @@ def update_graphs(self): self.app.load_data[core], label=f"Core {core}", alpha=0.7, - linestyle=line_style + linestyle=line_style, ) - - self.load_ax.set_ylabel('Load (%)', color='#cccccc') - self.load_ax.set_title('CPU Load Over Time', color='#cccccc') + + self.load_ax.set_ylabel("Load (%)", color="#cccccc") + self.load_ax.set_title("CPU Load Over Time", color="#cccccc") self.load_ax.set_ylim(0, 100) # 0-100% range for CPU load - self.load_ax.grid(True, alpha=0.3, color='#555555') - self.load_ax.tick_params(colors='#cccccc') - + self.load_ax.grid(True, alpha=0.3, color="#555555") + self.load_ax.tick_params(colors="#cccccc") + # Set x-axis labels to show elapsed time if len(time_labels) > 0: # Show every nth label to avoid crowding step = max(1, len(time_labels) // 10) self.load_ax.set_xticks(range(0, len(time_labels), step)) - self.load_ax.set_xticklabels([time_labels[i] for i in range(0, len(time_labels), step)]) - + self.load_ax.set_xticklabels( + [time_labels[i] for i in range(0, len(time_labels), step)] + ) + # Anchor legend outside plot area (bottom) if cores_to_show or -1 in cores_to_show: - legend = self.load_ax.legend(loc='upper center', bbox_to_anchor=(0.5, -0.25), ncol=4, framealpha=0.9) - legend.get_frame().set_facecolor('#2b2b2b') + legend = self.load_ax.legend( + loc="upper center", bbox_to_anchor=(0.5, -0.25), ncol=4, framealpha=0.9 + ) + legend.get_frame().set_facecolor("#2b2b2b") for text in legend.get_texts(): - text.set_color('#cccccc') - + text.set_color("#cccccc") + # Rotate x-axis labels for better readability for label in self.load_ax.get_xticklabels(): label.set_rotation(45) - label.set_ha('right') - + label.set_ha("right") + self.load_fig.tight_layout(pad=1.5) self.load_canvas.draw() - + def format_elapsed_time(self, seconds): """Format elapsed time for graph display""" if seconds < 60: @@ -1137,9 +1429,11 @@ def format_elapsed_time(self, seconds): minutes = int((seconds % 3600) // 60) return f"{hours}h{minutes}m" + def main(): app = CoreMonApp() return app.run(None) + if __name__ == "__main__": main() diff --git a/debian/coremon/usr/share/applications/coremon.desktop b/debian/coremon/usr/share/applications/coremon.desktop index ffe5912..50f33b2 100644 --- a/debian/coremon/usr/share/applications/coremon.desktop +++ b/debian/coremon/usr/share/applications/coremon.desktop @@ -1,7 +1,7 @@ [Desktop Entry] Name=CoreMon Comment=Monitor CPU temperature and load -Exec=python3 /home/spencer/Documents/GitHub/CascadeProjects/windsurf-project/coremon/main.py +Exec=coremon Icon=applications-system Terminal=false Type=Application diff --git a/debian/coremon/usr/share/doc/coremon/changelog.Debian.gz b/debian/coremon/usr/share/doc/coremon/changelog.Debian.gz index 9f9b2c0..ba4837e 100644 Binary files a/debian/coremon/usr/share/doc/coremon/changelog.Debian.gz and b/debian/coremon/usr/share/doc/coremon/changelog.Debian.gz differ diff --git a/debian/files b/debian/files index 0a7d5be..93f7c57 100644 --- a/debian/files +++ b/debian/files @@ -1,2 +1,2 @@ -coremon_1.0.0-1.2_amd64.buildinfo utils optional -coremon_1.0.0-1.2_amd64.deb utils optional +coremon_1.0.2-1_all.deb utils optional +coremon_1.0.2-1_amd64.buildinfo utils optional diff --git a/md5sums b/md5sums new file mode 100644 index 0000000..432a376 --- /dev/null +++ b/md5sums @@ -0,0 +1,13 @@ +a063dbcca9f5b9c7dbb74c45f2b8cf0a usr/bin/coremon +c6f6909bd8c899bb4410cde5e3438d8f usr/bin/coremon.sh +db3ff09f9797fa2fde01ede1b1c00a10 usr/lib/python3/dist-packages/coremon-1.0.2.egg-info/PKG-INFO +68b329da9893e34099c7d8ad5cb9c940 usr/lib/python3/dist-packages/coremon-1.0.2.egg-info/dependency_links.txt +7b5945e91fac3e9e174395ee16f3e730 usr/lib/python3/dist-packages/coremon-1.0.2.egg-info/entry_points.txt +cd849028afb2cb2a01717caec8bab65e usr/lib/python3/dist-packages/coremon-1.0.2.egg-info/requires.txt +d6b3addc3d572d81c8786a224f30db57 usr/lib/python3/dist-packages/coremon-1.0.2.egg-info/top_level.txt +db2c241749a9c9122488a2fa0b34ed9c usr/lib/python3/dist-packages/coremon/main.py +5e1f7bc6f276ecc2991614bc09420087 usr/lib/python3/dist-packages/cpumon/__init__.py +5134ab29569f4f8608a6c13c7fcf1cb7 usr/lib/python3/dist-packages/cpumon/main.py +68350531d35be40a469bd3065388940f usr/lib/python3/dist-packages/cpumon/setup.py +b376166792deb340907cb0156965c890 usr/share/applications/coremon.desktop +099389bb212c1827b73dc7f786132e01 usr/share/doc/coremon/changelog.Debian.gz diff --git a/postinst b/postinst new file mode 100755 index 0000000..efdef3d --- /dev/null +++ b/postinst @@ -0,0 +1,28 @@ +#!/bin/sh +set -e + +# Automatically added by dh_python3 +if command -v py3compile >/dev/null 2>&1; then + py3compile -p coremon +fi +if command -v pypy3compile >/dev/null 2>&1; then + pypy3compile -p coremon || true +fi + +# End automatically added section + +echo "CoreMon installation complete!" +echo "Note: User configuration will be created when you first run coremon." + + +# Automatically added by dh_python3 +if command -v py3compile >/dev/null 2>&1; then + py3compile -p coremon +fi +if command -v pypy3compile >/dev/null 2>&1; then + pypy3compile -p coremon || true +fi + +# End automatically added section + +exit 0 diff --git a/prerm b/prerm new file mode 100755 index 0000000..95cb9f5 --- /dev/null +++ b/prerm @@ -0,0 +1,56 @@ +#!/bin/sh +set -e + +echo "Cleaning up CoreMon configuration..." + +# Get the actual user who initiated the package removal +# When running as root during package removal, we need to find the original user +if [ -n "$SUDO_USER" ]; then + REAL_USER="$SUDO_USER" +elif [ -n "$USER" ] && [ "$USER" != "root" ]; then + REAL_USER="$USER" +else + # Try to find the user who owns the X session or a logged in user + REAL_USER=$(logname 2>/dev/null || echo "$(ls -la /home | head -n 2 | tail -n 1 | awk '{print $3}')") +fi + +# Remove autostart desktop file if it exists +AUTOSTART_FILE="/home/$REAL_USER/.config/autostart/coremon.desktop" +if [ -f "$AUTOSTART_FILE" ]; then + echo "Removing autostart desktop file for user $REAL_USER..." + rm -f "$AUTOSTART_FILE" +fi + +# Remove entire CoreMon configuration directory +CONFIG_DIR="/home/$REAL_USER/.config/coremon" +if [ -d "$CONFIG_DIR" ]; then + echo "Removing CoreMon configuration directory for user $REAL_USER..." + rm -rf "$CONFIG_DIR" +fi + +echo "CoreMon configuration cleanup complete." + +# Include debhelper cleanup (this is important!) + +# Automatically added by dh_python3 +if command -v py3clean >/dev/null 2>&1; then + # Check if the package is actually installed before trying to clean it + if dpkg-query -W -f='${Status}' coremon 2>/dev/null | grep -q "install ok installed"; then + py3clean -p coremon + else + echo "CoreMon package not found, skipping Python cleanup" + fi +else + # Check if the package is actually installed before trying to list its files + if dpkg-query -W -f='${Status}' coremon 2>/dev/null | grep -q "install ok installed"; then + dpkg -L coremon | sed -En -e '/^(.*)\/(.+)\.py$/s,,rm "\1/__pycache__/\2".*,e' + find /usr/lib/python3/dist-packages/ -type d -name __pycache__ -empty -print0 | xargs --null --no-run-if-empty rmdir + else + echo "CoreMon package not found, skipping Python cache cleanup" + fi +fi + +# End automatically added section + + +exit 0 diff --git a/setup.py b/setup.py index 3840333..67b3192 100644 --- a/setup.py +++ b/setup.py @@ -12,7 +12,7 @@ # Install the package setup( name="coremon", - version="1.0.1", + version="1.0.2", packages=find_packages(), include_package_data=True, install_requires=[