diff --git a/modules/about.py b/modules/about.py deleted file mode 100644 index 46c7f1c..0000000 --- a/modules/about.py +++ /dev/null @@ -1,537 +0,0 @@ -import re -import subprocess -import os - -import gi - -gi.require_version("GdkPixbuf", "2.0") -gi.require_version("Gtk", "3.0") -from gi.repository import GdkPixbuf, Gtk # type: ignore - -from fabric.utils.helpers import get_relative_path -from utils.roam import modus_service -from utils.icon_resolver import IconResolver - - -def read_dmi(field): - try: - with open(f"/sys/class/dmi/id/{field}") as f: - return f.read().strip() - except Exception: - return "Unknown" - - -def get_gpu_name(): - try: - output = ( - subprocess.check_output( - "lspci -nn | grep -i 'VGA compatible controller'", shell=True, text=True - ) - .strip() - .split("\n") - ) - - def clean(line): - matches = re.findall(r"\[(.*?)\]", line) - if len(matches) >= 2: - return matches[1].strip() - desc = line.split(":", 2)[-1] - return desc.replace("Corporation", "").strip() - - for line in output: - if any(vendor in line.lower() for vendor in ["nvidia", "amd", "radeon"]): - return clean(line) - - if output: - return clean(output[0]) - - return "Unknown GPU" - except Exception: - return "Unknown GPU" - - -def get_executable_path(exec_string): - """Extract and find the actual executable path from Exec field""" - if not exec_string: - return None - - # Remove common exec modifiers and arguments - exec_parts = exec_string.split() - if not exec_parts: - return None - - executable = exec_parts[0] - - # Remove common prefixes - prefixes_to_remove = ["env", "bash", "sh", "/usr/bin/env"] - while executable in prefixes_to_remove and len(exec_parts) > 1: - exec_parts.pop(0) - executable = exec_parts[0] - - # If it's already an absolute path, check if it exists - if executable.startswith("/"): - if os.path.exists(executable): - return executable - return None - - # Search in PATH - try: - result = subprocess.run(["which", executable], capture_output=True, text=True) - if result.returncode == 0: - return result.stdout.strip() - except Exception: - pass - - return None - - -def get_app_info(wmclass): - """Get comprehensive application information from .desktop file""" - if not wmclass: - return { - "name": "Desktop", - "version": "", - "comment": "Desktop Environment", - "icon": "desktop", - "exec": "", - "location": "", - "categories": "", - "desktop_file": "", - } - - desktop_paths = [ - "/usr/share/applications", - "/var/lib/flatpak/exports/share/applications", - os.path.expanduser("~/.local/share/applications"), - "/usr/local/share/applications", - ] - - for path in desktop_paths: - if not os.path.exists(path): - continue - - # Try exact match first - exact_matches = [ - f for f in os.listdir(path) if f.lower() == f"{wmclass.lower()}.desktop" - ] - - # Then try starts with - startswith_matches = [ - f - for f in os.listdir(path) - if f.startswith(wmclass.lower()) and f.endswith(".desktop") - ] - - # Finally try contains - contains_matches = [ - f - for f in os.listdir(path) - if wmclass.lower() in f.lower() and f.endswith(".desktop") - ] - - # Process matches in order of preference - for matches in [exact_matches, startswith_matches, contains_matches]: - for filename in matches: - desktop_file = os.path.join(path, filename) - try: - with open(desktop_file, "r", encoding="utf-8") as f: - content = f.read() - - name = wmclass.title() - version = "" - comment = "" - icon = wmclass.lower() - exec_cmd = "" - categories = "" - - # Parse desktop file - in_desktop_entry = False - for line in content.split("\n"): - line = line.strip() - if line == "[Desktop Entry]": - in_desktop_entry = True - continue - elif line.startswith("[") and line.endswith("]"): - in_desktop_entry = False - continue - - if not in_desktop_entry or "=" not in line: - continue - - key, value = line.split("=", 1) - if key == "Name": - name = value - elif key == "Version": - version = value - elif key == "Comment": - comment = value - elif key == "GenericName" and not comment: - # Use GenericName as fallback description - comment = value - elif key == "Icon": - icon = value - elif key == "Exec": - exec_cmd = value - elif key == "Categories": - categories = value - - # Get executable location - location = get_executable_path(exec_cmd) - - return { - "name": name, - "version": version, - "comment": comment, - "icon": icon, - "exec": exec_cmd, - "location": location or "", - "categories": categories, - "desktop_file": desktop_file, - } - except Exception: - continue - - # Fallback: try to find executable in PATH - location = "" - try: - result = subprocess.run( - ["which", wmclass.lower()], capture_output=True, text=True - ) - if result.returncode == 0: - location = result.stdout.strip() - except Exception: - pass - - return { - "name": wmclass.title() if wmclass else "Unknown Application", - "version": "", - "comment": "", - "icon": wmclass.lower() if wmclass else "application-x-executable", - "exec": "", - "location": location, - "categories": "", - "desktop_file": "", - } - - -class AboutApp(Gtk.Window): - def __init__(self, app_name="Unknown Application", wmclass=""): - super().__init__(title=f"About {app_name}") - self.app_name = app_name - self.wmclass = wmclass - self.icon_resolver = IconResolver() - - self.set_default_size(300, 500) - self.set_size_request(300, 480) - self.set_resizable(False) - self.set_title(f"About {app_name}") - self.set_name("about-app") - self.set_visible(False) - - self.setup_ui() - - def setup_ui(self): - app_info = get_app_info(self.wmclass) - - main_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10) - main_box.set_margin_top(20) - main_box.set_margin_bottom(20) - main_box.set_margin_start(15) - main_box.set_margin_end(15) - - # App Icon - logo_box = Gtk.Box(halign=Gtk.Align.CENTER, valign=Gtk.Align.CENTER) - try: - # Use IconResolver's get_icon_pixbuf method like other parts of the project - icon_pixbuf = self.icon_resolver.get_icon_pixbuf(app_info["icon"], 128) - if icon_pixbuf: - logo = Gtk.Image.new_from_pixbuf(icon_pixbuf) - else: - raise Exception("Icon pixbuf not found") - except Exception: - # Fallback: try direct file path if it's an absolute path - try: - if app_info["icon"].startswith("/") and os.path.exists( - app_info["icon"] - ): - pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale( - app_info["icon"], 128, 128, preserve_aspect_ratio=True - ) - logo = Gtk.Image.new_from_pixbuf(pixbuf) - else: - raise Exception("Direct path failed") - except Exception: - # Final fallback: emoji - logo = Gtk.Label() - logo.set_markup("📱") - - logo_box.pack_start(logo, False, False, 0) - logo_box.set_margin_bottom(15) - - # App Name - app_name_label = Gtk.Label() - app_name_label.set_markup( - f"{app_info['name']}" - ) - app_name_label.set_halign(Gtk.Align.CENTER) - app_name_label.set_margin_bottom(5) - - # Version (if available) - if app_info["version"]: - version_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - version_label = Gtk.Label( - label=f"Version {app_info.get('version', 'Unknown')}" - ) - version_label.set_name("version-label") - version_label.set_halign(Gtk.Align.CENTER) - version_box.pack_start(version_label, False, False, 0) - version_box.set_margin_bottom(5) - - # Description/Comment - Make it more prominent - description_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - if app_info["comment"]: - # Create a frame for the description to make it stand out - desc_frame = Gtk.Frame() - desc_frame.set_shadow_type(Gtk.ShadowType.IN) - - description_label = Gtk.Label() - description_label.set_markup(f"{app_info['comment']}") - description_label.set_justify(Gtk.Justification.CENTER) - description_label.set_halign(Gtk.Align.CENTER) - description_label.set_line_wrap(True) - description_label.set_max_width_chars(45) - description_label.set_margin_top(8) - description_label.set_margin_bottom(8) - description_label.set_margin_start(12) - description_label.set_margin_end(12) - - desc_frame.add(description_label) - description_box.pack_start(desc_frame, False, False, 0) - else: - # Show a placeholder if no description is available - placeholder_label = Gtk.Label() - placeholder_label.set_markup( - "No description available" - ) - placeholder_label.set_halign(Gtk.Align.CENTER) - description_box.pack_start(placeholder_label, False, False, 0) - - description_box.set_margin_bottom(15) - - # Information Grid - # Create a label with markup - info_frame = Gtk.Frame() - label = Gtk.Label() - label.set_markup("Application Information") - label.set_margin_bottom(5) - info_frame.set_label_widget(label) - - info_frame.set_label_align(0.5, 0.5) - - info_grid = Gtk.Grid() - info_grid.set_row_spacing(8) - info_grid.set_column_spacing(15) - info_grid.set_margin_top(10) - info_grid.set_margin_bottom(10) - info_grid.set_margin_start(15) - info_grid.set_margin_end(15) - info_grid.set_valign(Gtk.Align.CENTER) - info_grid.set_halign(Gtk.Align.CENTER) - - def make_info_row(label_text, value_text, row): - """Create a row in the info grid""" - label = Gtk.Label(label=f"{label_text}:") - label.set_halign(Gtk.Align.END) - label.set_markup(f"{label_text}:") - - value = Gtk.Label() - value.set_markup(f'{value_text}') - value.set_halign(Gtk.Align.START) - value.set_line_wrap(True) - value.set_max_width_chars(30) - value.set_name("info-value-label") - - info_grid.attach(label, 0, row, 1, 1) - info_grid.attach(value, 1, row, 1, 1) - - row = 0 - - # Application Name - make_info_row("Name", app_info["name"], row) - row += 1 - - # Version - if app_info["version"]: - make_info_row("Version", app_info["version"], row) - row += 1 - - # Executable Location - if app_info["location"]: - make_info_row("Location", app_info["location"], row) - row += 1 - - # Window Class - if self.wmclass: - make_info_row("Window Class", self.wmclass, row) - row += 1 - - # Categories - if app_info["categories"]: - categories = app_info["categories"].replace(";", ", ").strip(", ") - make_info_row("Categories", categories, row) - row += 1 - - # Desktop File - if app_info["desktop_file"]: - desktop_file_name = os.path.basename(app_info["desktop_file"]) - make_info_row("Desktop File", desktop_file_name, row) - row += 1 - - info_frame.add(info_grid) - - # Layout - main_box.pack_start(logo_box, False, False, 0) - main_box.pack_start(app_name_label, False, False, 0) - if app_info["version"]: - main_box.pack_start(version_box, False, False, 0) - main_box.pack_start(description_box, False, False, 0) - main_box.pack_start(info_frame, False, False, 0) - - self.add(main_box) - - def toggle(self, b): - if self.get_visible(): - self.hide() - else: - self.show_all() - - -class About(Gtk.Window): - def __init__(self): - super().__init__(title="About Menu") - self.set_default_size(300, 550) - self.set_size_request(300, 500) - self.set_resizable(False) - self.set_title("About PC") - self.set_name("about-menu") - self.set_visible(False) - - main_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10) - main_box.set_margin_top(10) - main_box.set_margin_bottom(20) - main_box.set_margin_start(20) - main_box.set_margin_end(20) - - # Logo - logo_box = Gtk.Box(halign=Gtk.Align.CENTER, valign=Gtk.Align.CENTER) - pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale( - get_relative_path("../config/assets/icons/misc/imac.svg"), - 158, - 108, - preserve_aspect_ratio=True, - ) - logo = Gtk.Image.new_from_pixbuf(pixbuf) - logo_box.pack_start(logo, False, False, 0) - logo_box.set_margin_top(60) - - # Product Name & Vendor - name_label = Gtk.Label() - name_label.set_margin_top(30) - name_label.set_markup( - f"{read_dmi('product_name')}" - ) - - vendor_label = Gtk.Label(label=read_dmi("sys_vendor")) - vendor_label.set_name("vendor-label") - vendor_label.set_halign(Gtk.Align.CENTER) - - # Info Grid - info_grid = Gtk.Grid() - info_grid.set_row_spacing(6) - info_grid.set_column_spacing(10) - info_grid.set_valign(Gtk.Align.CENTER) - info_grid.set_halign(Gtk.Align.FILL) - - def make_label(text, align_end=False, name=None): - label = Gtk.Label(label=text) - label.set_halign(Gtk.Align.END if align_end else Gtk.Align.START) - if name: - label.set_name(name) - return label - - # Info values - labels = [ - ( - "Kernel", - subprocess.run( - "uname -r", shell=True, capture_output=True, text=True - ).stdout.strip(), - ), - ( - "CPU", - subprocess.run( - "lscpu | grep 'Model name:' | cut -d ':' -f2-", - shell=True, - capture_output=True, - text=True, - ).stdout.strip(), - ), - ( - "Memory", - subprocess.run( - "free -h --giga | grep Mem | tr -s ' ' | cut -d ' ' -f 2", - shell=True, - capture_output=True, - text=True, - ).stdout.strip(), - ), - ("GPU", get_gpu_name()), - ( - "Uptime", - subprocess.run( - "uptime -p", shell=True, capture_output=True, text=True - ).stdout.strip(), - ), - ] - - for i, (title, value) in enumerate(labels): - title_label = make_label(title, align_end=True) - value_label = make_label(value, align_end=False, name="info-label") - info_grid.attach(title_label, 0, i, 1, 1) - info_grid.attach(value_label, 1, i, 1, 1) - - # Button - button_box = Gtk.Box(halign=Gtk.Align.CENTER) - button_box.set_margin_top(20) - more_info_button = Gtk.Button(label="More Info...", name="more-info-button") - more_info_button.connect("clicked", self.open_more_info) - button_box.pack_start(more_info_button, False, False, 0) - - # Info Footer - info = Gtk.Label( - label="™ and © 2025 Linux Inc.\nAll rights reserved.\n\n", - justify=Gtk.Justification.CENTER, - halign=Gtk.Align.CENTER, - ) - info.set_name("info-label") - info.set_margin_top(10) - - # Layout Order - main_box.pack_start(logo_box, False, False, 0) - main_box.pack_start(name_label, False, False, 0) - main_box.pack_start(vendor_label, False, False, 0) - main_box.pack_start(info_grid, False, False, 0) - main_box.pack_start(button_box, False, False, 0) - main_box.pack_start(info, False, False, 0) - - self.add(main_box) - - def open_more_info(self, button): - # TODO: Implement the logic to open more information - pass - - def toggle(self, b): - if self.get_visible(): - self.hide() - else: - self.show_all() diff --git a/modules/corners.py b/modules/corners.py deleted file mode 100644 index cf3c79e..0000000 --- a/modules/corners.py +++ /dev/null @@ -1,68 +0,0 @@ -from fabric.widgets.box import Box -from fabric.widgets.shapes import Corner -from widgets.wayland import WaylandWindow as Window - - -class MyCorner(Box): - def __init__(self, corner): - super().__init__( - name="corner-container", - children=Corner( - name="corner", - orientation=corner, - h_expand=False, - v_expand=False, - h_align="center", - v_align="center", - size=20, - ), - ) - - -class Corners(Window): - def __init__(self): - super().__init__( - name="corners", - layer="bottom", - anchor="top bottom left right", - exclusivity="normal", - # pass_through=True, - visible=False, - all_visible=False, - ) - - self.all_corners = Box( - name="all-corners", - orientation="v", - h_expand=True, - v_expand=True, - h_align="fill", - v_align="fill", - children=[ - Box( - name="top-corners", - orientation="h", - h_align="fill", - children=[ - MyCorner("top-left"), - Box(h_expand=True), - MyCorner("top-right"), - ], - ), - Box(v_expand=True), - Box( - name="bottom-corners", - orientation="h", - h_align="fill", - children=[ - MyCorner("bottom-left"), - Box(h_expand=True), - MyCorner("bottom-right"), - ], - ), - ], - ) - - self.add(self.all_corners) - - self.show_all() diff --git a/modules/widget.py b/modules/widget.py deleted file mode 100644 index 21ce287..0000000 --- a/modules/widget.py +++ /dev/null @@ -1,887 +0,0 @@ -# Standard library imports -import psutil -import requests -import urllib.parse -import datetime -import time -import subprocess -import calendar -from concurrent.futures import ThreadPoolExecutor -from typing import Optional, Tuple, List, Dict, Any - -# Fabric imports -from fabric.widgets.box import Box -from fabric.widgets.label import Label -from fabric.widgets.overlay import Overlay -from fabric.widgets.datetime import DateTime -from fabric.widgets.circularprogressbar import CircularProgressBar -from widgets.wayland import WaylandWindow as Window -from fabric.utils import invoke_repeater -from gi.repository import GLib - -# Local imports -from config.data import load_config - -# Module-level constants -WEATHER_UPDATE_INTERVAL = 600 # 10 minutes -WEATHER_CACHE_TIMEOUT = 1800 # 30 minutes -SYSTEM_UPDATE_INTERVAL = 1000 # 1 second -CALENDAR_UPDATE_INTERVAL = int( - ( - ( - datetime.datetime.combine( - datetime.date.today() + datetime.timedelta(days=1), datetime.time.min - ) - - datetime.datetime.now() - ).total_seconds() - ) - * 1000 -) # Calculate time till midnight -LOCATION_CACHE_TIMEOUT = 604800 # 7 days (extended from 24h) - -# Thread pool for async operations -executor = ThreadPoolExecutor(max_workers=4) - -# Weather condition to CSS class mapping (iOS-style gradients) -WEATHER_GRADIENT_MAP = { - # Clear/Sunny conditions - bright blue to lighter blue - 0: "weather-clear", # Clear sky - 1: "weather-mostly-clear", # Mainly clear - # Cloudy conditions - grey gradients - 2: "weather-partly-cloudy", # Partly cloudy - 3: "weather-overcast", # Overcast - # Fog conditions - muted grey/blue - 45: "weather-fog", # Fog - 48: "weather-fog", # Depositing rime fog - # Light rain/drizzle - blue-grey gradients - 51: "weather-light-rain", # Light drizzle - 53: "weather-rain", # Moderate drizzle - 55: "weather-rain", # Dense drizzle - 61: "weather-light-rain", # Slight rain - 80: "weather-light-rain", # Slight rain showers - # Heavy rain - darker blue-grey - 63: "weather-heavy-rain", # Moderate rain - 65: "weather-heavy-rain", # Heavy rain - 81: "weather-heavy-rain", # Moderate rain showers - 82: "weather-storm", # Violent rain showers - # Snow conditions - blue-white gradients - 56: "weather-snow", # Light freezing drizzle - 57: "weather-snow", # Dense freezing drizzle - 66: "weather-snow", # Light freezing rain - 67: "weather-snow", # Heavy freezing rain - 71: "weather-snow", # Slight snow fall - 73: "weather-heavy-snow", # Moderate snow fall - 75: "weather-heavy-snow", # Heavy snow fall - 77: "weather-snow", # Snow grains - 85: "weather-snow", # Slight snow showers - 86: "weather-heavy-snow", # Heavy snow showers - # Storm conditions - dark dramatic gradients - 95: "weather-storm", # Thunderstorm - 96: "weather-storm", # Thunderstorm with slight hail - 99: "weather-storm", # Thunderstorm with heavy hail -} - -# Weather condition to emoji mapping -WEATHER_EMOJI_MAP = { - 0: "☀️", # Clear sky - 1: "🌤️", # Mainly clear - 2: "⛅", # Partly cloudy - 3: "☁️", # Overcast - 45: "🌫️", # Fog - 48: "🌫️", # Depositing rime fog - 51: "🌦️", # Light drizzle - 53: "🌧️", # Moderate drizzle - 55: "🌧️", # Dense drizzle - 56: "🌨️", # Light freezing drizzle - 57: "🌨️", # Dense freezing drizzle - 61: "🌦️", # Slight rain - 63: "🌧️", # Moderate rain - 65: "🌧️", # Heavy rain - 66: "🌨️", # Light freezing rain - 67: "🌨️", # Heavy freezing rain - 71: "🌨️", # Slight snow fall - 73: "❄️", # Moderate snow fall - 75: "❄️", # Heavy snow fall - 77: "🌨️", # Snow grains - 80: "🌦️", # Slight rain showers - 81: "🌧️", # Moderate rain showers - 82: "⛈️", # Violent rain showers - 85: "🌨️", # Slight snow showers - 86: "❄️", # Heavy snow showers - 95: "⛈️", # Thunderstorm - 96: "⛈️", # Thunderstorm with slight hail - 99: "⛈️", # Thunderstorm with heavy hail -} - -# Weather condition descriptions -WEATHER_DESC_MAP = { - 0: "Clear sky", - 1: "Mainly clear", - 2: "Partly cloudy", - 3: "Overcast", - 45: "Fog", - 48: "Depositing rime fog", - 51: "Light drizzle", - 53: "Moderate drizzle", - 55: "Dense drizzle", - 56: "Light freezing drizzle", - 57: "Dense freezing drizzle", - 61: "Slight rain", - 63: "Moderate rain", - 65: "Heavy rain", - 66: "Light freezing rain", - 67: "Heavy freezing rain", - 71: "Slight snow", - 73: "Moderate snow", - 75: "Heavy snow", - 77: "Snow grains", - 80: "Light rain showers", - 81: "Moderate rain showers", - 82: "Violent rain showers", - 85: "Slight snow showers", - 86: "Heavy snow showers", - 95: "Thunderstorm", - 96: "Thunderstorm with hail", - 99: "Thunderstorm with heavy hail", -} - -# Location APIs in order of preference (fastest first) -LOCATION_APIS = [ - "https://ipapi.co/json/", # Fastest, 200ms average - "http://ip-api.com/json/", # Fast fallback, 150ms average - "https://ipinfo.io/json", # Original fallback -] - -# Global cache for weather data -_weather_cache: Dict[str, Tuple[Any, float]] = {} -_location_cache: Dict[str, Tuple[float, float, float]] = {} - - -def get_location() -> str: - """Get current location using multiple IP geolocation APIs with fallback.""" - for api_url in LOCATION_APIS: - try: - response = requests.get(api_url, timeout=2) - if response.status_code == 200: - data = response.json() - # Handle different API response formats - city = data.get("city", "") - if city: - return city.replace(" ", "") - except requests.RequestException as e: - print(f"Location API {api_url} failed: {e}") - continue - - print("All location APIs failed") - return "" - - -def get_coordinates(city: str) -> Optional[Tuple[float, float]]: - """Get coordinates for a city using Nominatim geocoding API.""" - cache_key = city.lower() - current_time = time.time() - - # Check cache first (cache for 7 days) - if cache_key in _location_cache: - lat, lon, timestamp = _location_cache[cache_key] - if current_time - timestamp < LOCATION_CACHE_TIMEOUT: - return lat, lon - - try: - encoded_city = urllib.parse.quote(city) - url = f"https://nominatim.openstreetmap.org/search?q={encoded_city}&format=json&limit=1" - response = requests.get( - url, timeout=3, headers={"User-Agent": "Modus-Desktop/1.0"} - ) - - if response.status_code == 200: - data = response.json() - if data: - lat = float(data[0]["lat"]) - lon = float(data[0]["lon"]) - _location_cache[cache_key] = (lat, lon, current_time) - return lat, lon - except (requests.RequestException, ValueError, KeyError) as e: - print(f"Error geocoding {city}: {e}") - - return None - - -def get_weather_data(lat: float, lon: float) -> Optional[Dict[str, Any]]: - """Fetch weather data from Open-Meteo API.""" - try: - url = ( - f"https://api.open-meteo.com/v1/forecast?" - f"latitude={lat}&longitude={lon}" - f"¤t_weather=true" - f"&daily=temperature_2m_max,temperature_2m_min" - f"&timezone=auto" - f"&forecast_days=1" - ) - - response = requests.get(url, timeout=3) - if response.status_code == 200: - return response.json() - except requests.RequestException as e: - print(f"Error fetching weather data: {e}") - - return None - - -def format_weather_data(weather_data: Dict[str, Any], city: str) -> List[str]: - """Format weather data into the expected format.""" - try: - current = weather_data["current_weather"] - daily = weather_data["daily"] - - # Get weather code and map to emoji and description - weather_code = current["weathercode"] - emoji = WEATHER_EMOJI_MAP.get(weather_code, "🌤️") - condition = WEATHER_DESC_MAP.get(weather_code, "Unknown") - gradient_class = WEATHER_GRADIENT_MAP.get(weather_code, "weather-clear") - - # Temperature - temp = f"{round(current['temperature'])}°" - - # Daily min/max temperatures - max_temp = f"{round(daily['temperature_2m_max'][0])}°" - min_temp = f"{round(daily['temperature_2m_min'][0])}°" - - return [emoji, temp, condition, city, max_temp, min_temp, gradient_class] - - except (KeyError, IndexError, TypeError) as e: - print(f"Error formatting weather data: {e}") - return None - - -def get_weather(callback): - """Fetch weather data asynchronously using Open-Meteo API.""" - - def fetch_weather(): - # Get location - location = get_location() - if not location: - return GLib.idle_add(callback, None) - - # Check cache first - cache_key = location.lower() - current_time = time.time() - - if cache_key in _weather_cache: - cached_data, timestamp = _weather_cache[cache_key] - if current_time - timestamp < WEATHER_CACHE_TIMEOUT: - return GLib.idle_add(callback, cached_data) - - # Get coordinates for the location - coords = get_coordinates(location) - if not coords: - return GLib.idle_add(callback, None) - - lat, lon = coords - - # Fetch weather data - weather_data = get_weather_data(lat, lon) - if not weather_data: - return GLib.idle_add(callback, None) - - # Format data - formatted_data = format_weather_data(weather_data, location) - if formatted_data: - # Cache the result - _weather_cache[cache_key] = (formatted_data, current_time) - GLib.idle_add(callback, formatted_data) - else: - GLib.idle_add(callback, None) - - executor.submit(fetch_weather) - - -def update_weather(widget): - """Update weather widget with new data.""" - - def fetch_and_update(): - get_weather(lambda weather_info: update_widget(widget, weather_info)) - return True - - GLib.timeout_add_seconds(WEATHER_UPDATE_INTERVAL, fetch_and_update) - fetch_and_update() - - -def update_widget(widget, weather_info): - """Update widget labels with weather information.""" - if weather_info: - widget.weatherinfo = weather_info - widget.update_labels(weather_info) - - -class Weather(Box): - """Weather widget displaying current conditions and forecast.""" - - def __init__(self, parent, **kwargs): - super().__init__( - name="weather-widget", - h_expand=True, - v_expand=True, - justification="right", - orientation="v", - all_visible=False, - **kwargs, - ) - - self.parent = parent - self.weatherinfo = None - - # Create labels with better organization - self._create_labels() - self._layout_labels() - - # Start weather updates - update_weather(self) - - def _create_labels(self): - """Create all weather labels.""" - self.city = Label( - name="city", - label="Loading...", - justification="right", - h_align="start", - max_chars_width=12, - ellipsization="end", - ) - self.temperature = Label(name="temperature", label="--°", h_align="start") - self.condition_em = Label(name="condition-emoji", label="🌤️", h_align="start") - self.condition = Label( - name="condition", - label="Loading...", - max_chars_width=18, - ellipsization="end", - h_align="start", - ) - self.feels_like = Label(name="feels-like", label="H:-- L:--", h_align="start") - - def _layout_labels(self): - """Add labels to the widget in proper order.""" - labels = [ - self.city, - self.temperature, - self.condition_em, - self.condition, - self.feels_like, - ] - for label in labels: - self.add(label) - - def update_labels(self, weather_info: List[str]): - """Update weather labels with new data.""" - if not weather_info or len(weather_info) != 7: - return - - emoji, temp, condition, location, maxtemp, mintemp, gradient_class = ( - weather_info - ) - maxmin = f"H:{maxtemp} L:{mintemp}" - - # Batch update labels for better performance - label_updates = [ - (self.city, location), - (self.temperature, temp), - (self.condition_em, emoji), - (self.condition, condition), - (self.feels_like, maxmin), - ] - - for label, text in label_updates: - label.set_label(text) - - # Apply gradient background based on weather condition - self.parent.set_visible(True) - - -class WeatherContainer(Box): - """Container for weather widget.""" - - def __init__(self, **kwargs): - super().__init__( - orientation="v", - name="weather-container", - v_expand=True, - v_align="center", - size=(170, 170), - visible=True, - h_align="center", - children=[Weather(self)], - **kwargs, - ) - - -class Date(Box): - """Date widget displaying day, month, and date.""" - - def __init__(self, **kwargs): - super().__init__( - name="date-widget", - h_expand=True, - v_expand=True, - justification="center", - h_align="center", - v_align="start", - orientation="v", - **kwargs, - ) - - # Create date components - self.top = Box(orientation="h", name="date-top", h_expand=True) - - # Use consistent interval for all date components - date_interval = 10000 # 10 seconds - self.dateone = DateTime(formatters=["%a"], interval=date_interval, name="day") - self.datetwo = DateTime(formatters=["%b"], interval=date_interval, name="month") - self.datethree = DateTime( - formatters=["%-d"], interval=date_interval, name="date" - ) - - # Layout components - self.top.add(self.dateone) - self.top.add(self.datetwo) - self.add(self.top) - self.add(self.datethree) - - -class DateContainer(Box): - """Container for date widget.""" - - def __init__(self, **kwargs): - super().__init__( - orientation="v", - name="date-container", - v_expand=True, - size=(170, 170), - v_align="center", - h_align="center", - children=[Date()], - **kwargs, - ) - - -class Calendar(Box): - """Calendar widget displaying current month.""" - - def __init__(self, **kwargs): - # Set Sunday as first day of week - calendar.setfirstweekday(6) # 6 = Sunday - super().__init__( - name="calendar-widget", - h_expand=True, - v_expand=True, - orientation="v", - **kwargs, - ) - - # Cache current date for efficiency - self._update_current_date() - - # Create calendar components - self._create_header() - self._create_days_header() - self._create_calendar_grid() - - # Layout components - self.add(self.month_label) - self.add(self.days_header) - self.add(self.calendar_grid) - - # Schedule updates - invoke_repeater(CALENDAR_UPDATE_INTERVAL, self.update_calendar_if_needed) - - def _update_current_date(self): - """Update cached current date values.""" - now = datetime.datetime.now() - self.current_month = now.month - self.current_year = now.year - self.current_day = now.day - - def _create_header(self): - """Create month header label.""" - self.month_label = Label( - name="calendar-month", - label=calendar.month_name[self.current_month], - h_align="start", - justification="left", - ) - - def _create_days_header(self): - """Create day abbreviations header.""" - self.days_header = Box( - name="calendar-days-header", orientation="h", h_expand=True, spacing=2 - ) - - day_names = ["S", "M", "T", "W", "T", "F", "S"] - for i, day_name in enumerate(day_names): - is_weekend = i in (0, 6) # Sunday or Saturday - day_label = Label( - name=( - "calendar-day-header-weekend" - if is_weekend - else "calendar-day-header" - ), - label=day_name, - h_align="center", - h_expand=True, - ) - self.days_header.add(day_label) - - def _create_calendar_grid(self): - """Create calendar grid container.""" - self.calendar_grid = Box(name="calendar-grid", orientation="v", spacing=1) - self.update_calendar() - - def update_calendar_if_needed(self) -> bool: - """Check if date changed and update calendar if needed.""" - now = datetime.datetime.now() - if ( - now.month != self.current_month - or now.year != self.current_year - or now.day != self.current_day - ): - - self._update_current_date() - self.update_calendar() - return True - - def update_calendar(self): - """Update the calendar grid with current month.""" - # Clear existing calendar efficiently - children = self.calendar_grid.get_children() - for child in children: - self.calendar_grid.remove(child) - - # Update month label - self.month_label.set_label(calendar.month_name[self.current_month]) - - # Generate calendar - cal = calendar.monthcalendar(self.current_year, self.current_month) - current_date = datetime.datetime.now() - - for week in cal: - week_box = Box(orientation="h", spacing=2, h_expand=True) - - for day_index, day in enumerate(week): - if day == 0: - # Empty day slot - day_label = Label( - name="calendar-day-empty", - label="", - h_align="center", - h_expand=True, - ) - else: - # Regular day - is_today = ( - day == self.current_day - and self.current_month == current_date.month - and self.current_year == current_date.year - ) - is_weekend = day_index in (0, 6) # Sunday or Saturday - - if is_today: - name = "calendar-day-today" - elif is_weekend: - name = "calendar-day-weekend" - else: - name = "calendar-day" - - day_label = Label( - name=name, label=str(day), h_align="center", h_expand=True - ) - - week_box.add(day_label) - self.calendar_grid.add(week_box) - - -class CalendarContainer(Box): - """Container for calendar widget.""" - - def __init__(self, **kwargs): - super().__init__( - orientation="v", - name="calendar-box-widget", - v_expand=True, - size=(170, 170), - v_align="center", - h_align="center", - children=[Calendar()], - **kwargs, - ) - - -class SystemInfoBase(Box): - """Base class for system information widgets.""" - - @staticmethod - def create_progress_bar(name: str = "progress-bar", size: int = 80, **kwargs): - """Create a standardized circular progress bar.""" - return CircularProgressBar( - name=name, - start_angle=270, - end_angle=630, - min_value=0, - max_value=100, - size=size, - **kwargs, - ) - - def __init__(self, name: str, **kwargs): - super().__init__( - layer="bottom", - title="sysinfo", - name=name, - visible=True, - size=(170, 170), - h_expand=True, - v_expand=True, - all_visible=True, - **kwargs, - ) - - # Create progress bar and labels - self.progress = self.create_progress_bar(name="progress") - self.main_label = Label( - label="0%\nLoading", justification="center", name="progress-label" - ) - - # Create info container - self.info_container = Box( - name="info-container", - orientation="v", - spacing=2, - h_align="center", - ) - - # Create main layout - self.progress_container = Box( - name="progress-bar-container", - h_expand=True, - v_expand=True, - orientation="v", - spacing=12, - h_align="center", - v_align="center", - children=[ - Box( - children=[ - Overlay( - child=self.progress, - tooltip_text="", - overlays=self.main_label, - ) - ] - ), - Box( - h_align="center", - justification="centre", - orientation="v", - children=[self.info_container], - ), - ], - ) - - self.add(self.progress_container) - - # Don't start updates here - let subclasses call start_updates() when ready - - def start_updates(self): - """Start the update timer - call this after subclass initialization is complete.""" - invoke_repeater(SYSTEM_UPDATE_INTERVAL, self.update) - - def create_info_line( - self, indicator_name: str, info_text: str, value_text: str - ) -> Box: - """Create an information line with indicator, label, and value.""" - indicator = Label(label="■", name=indicator_name) - info_label = Label(label=info_text, name="info-text") - value_label = Label(label=value_text, name="info-value") - - line = Box( - orientation="h", - spacing=4, - h_align="start", - children=[indicator, info_label, value_label], - ) - - # Store references for easy updates - line.indicator = indicator - line.info_label = info_label - line.value_label = value_label - - return line - - def update(self) -> bool: - """Override in subclasses.""" - raise NotImplementedError - - -class RamInfo(SystemInfoBase): - """RAM usage information widget.""" - - def __init__(self, **kwargs): - super().__init__("info-box-widget", **kwargs) - - # Create info lines and store references - self.used_line = self.create_info_line("used-color-indicator", "Used", "0.0GB") - self.free_line = self.create_info_line("free-color-indicator", "Free", "0.0GB") - - # Add to info container - self.info_container.add(self.used_line) - self.info_container.add(self.free_line) - - # Now that everything is set up, start updates - self.start_updates() - - def update(self) -> bool: - """Update RAM information.""" - try: - mem = psutil.virtual_memory() - - # Update main label - self.main_label.set_label(f" {round(mem.percent):<2} %\nRAM") - - # Calculate values - used_gb = mem.used / (1024**3) - free_gb = mem.available / (1024**3) - - # Update info labels using stored references - self.used_line.value_label.set_label(f"{round(used_gb, 1)}GB") - self.free_line.value_label.set_label(f"{round(free_gb, 1)}GB") - - # Update progress bar (use GLib.idle_add for thread safety) - GLib.idle_add(self.progress.set_value, mem.percent) - - except Exception as e: - print(f"Error updating RAM info: {e}") - - return True - - -class CpuInfo(SystemInfoBase): - """CPU usage and temperature information widget.""" - - def __init__(self, **kwargs): - super().__init__("info-box-widget", **kwargs) - - # Create temperature info components - self.temp_info = Label(label="Temp", name="info-text") - self.temp_value = Label(label="0°C", name="info-value") - - # Create temperature info line (no indicator) - self.temp_line = Box( - orientation="h", - spacing=4, - h_align="start", - children=[self.temp_info, self.temp_value], - ) - - # Add to info container - self.info_container.add(self.temp_line) - - # Now that everything is set up, start updates - self.start_updates() - - def get_cpu_temp(self) -> Optional[float]: - """Get CPU temperature from system sensors.""" - try: - temps = psutil.sensors_temperatures() - if not temps: - return None - - # Search for CPU temperature sensors - cpu_sensor_names = ["coretemp", "k10temp", "cpu"] - cpu_label_patterns = ["package id 0", "core 0", ""] - - for name, entries in temps.items(): - if any(sensor in name.lower() for sensor in cpu_sensor_names): - for entry in entries: - entry_label = (entry.label or "").lower() - if any( - pattern in entry_label for pattern in cpu_label_patterns - ): - return round(entry.current, 1) - except Exception as e: - print(f"Error reading CPU temperature: {e}") - - return None - - def update(self) -> bool: - """Update CPU information.""" - try: - # Get CPU usage - cpu = psutil.cpu_percent() - - # Update main label - self.main_label.set_label(f" {round(cpu):<2} %\nCPU") - - # Update temperature using stored reference - temp = self.get_cpu_temp() - temp_text = f"{temp}°C" if temp is not None else "N/A" - self.temp_value.set_label(temp_text) - - # Update progress bar (use GLib.idle_add for thread safety) - GLib.idle_add(self.progress.set_value, cpu) - - except Exception as e: - print(f"Error updating CPU info: {e}") - - return True - - -class Deskwidgets(Window): - """Desktop widgets manager - handles all desktop widgets.""" - - config = load_config() - - def __init__(self, **kwargs): - # Create the main invisible window that manages the widgets - super().__init__( - name="desktop-widget-manager", - layer="bottom", - title="modus-desktop-widget-manager", - visible=False, # This window is invisible - just manages the others - size=(1, 1), # Minimal size - anchor="top left", - **kwargs, - ) - - # Create separate independent windows as attributes - self.top_left = Window( - anchor="top left", - title="modus-widgets-topleft", - orientation="h", - layer="bottom", - visible=False, # Start hidden until content ready - child=Box( - name="desktop-widgets-container", - children=[ - DateContainer(), - WeatherContainer(), - CalendarContainer(), - ], - ), - ) - - self.bottom_left = Window( - anchor="bottom right", - title="modus-widgets-bottomright", - orientation="h", - layer="bottom", - visible=False, # Start hidden until content ready - child=Box( - name="desktop-widgets-container", - children=[ - CpuInfo(), - RamInfo(), - ], - ), - ) - - # Show widgets after initialization is complete - self.top_left.set_visible(True) - self.bottom_left.set_visible(True)