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)