refactor(modules): remove unused about module
The about.py module was deprecated and replaced by newer implementations; its removal cleans up dead code. New Features - No new feature introduced Enhancements - No functional enhancements Bug Fixes - No bug fixes
This commit is contained in:
3 files changed
-1492
No files matched your search
@@ -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("<span size='72000'>📱</span>")
|
||||
|
||||
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"<b><span size='18000'>{app_info['name']}</span></b>"
|
||||
)
|
||||
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"<i><b>{app_info['comment']}</b></i>")
|
||||
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(
|
||||
"<i><span foreground='#888888'>No description available</span></i>"
|
||||
)
|
||||
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("<b><u>Application Information</u></b>")
|
||||
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"<b>{label_text}:</b>")
|
||||
|
||||
value = Gtk.Label()
|
||||
value.set_markup(f'<span foreground="#727272">{value_text}</span>')
|
||||
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"<b><span size='16000'>{read_dmi('product_name')}</span></b>"
|
||||
)
|
||||
|
||||
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()
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
Reference in new issue
Block a user