Files
Modus/modules/widget.py
T

653 lines
20 KiB
Python

import psutil
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
import requests
import urllib.parse
import datetime
from gi.repository import GLib
from concurrent.futures import ThreadPoolExecutor
import time
from config.data import load_config
import subprocess
import calendar
executor = ThreadPoolExecutor(max_workers=4)
def get_location():
"""Fetch location from config file or IP API asynchronously."""
for attempt in range(5):
try:
response = requests.get("https://ipinfo.io/json", timeout=3)
if response.status_code == 200:
return response.json().get("city", "").replace(" ", "")
except requests.RequestException as e:
print(f"Error getting location: {e}")
if attempt < 4:
time.sleep(10)
else:
return ""
def get_location_async(callback):
"""Fetch location asynchronously to prevent UI freeze."""
executor.submit(lambda: GLib.idle_add(callback, get_location()))
def get_weather(callback):
"""Fetch weather data asynchronously and update UI."""
def fetch_weather():
location = get_location()
if not location:
return GLib.idle_add(callback, None)
encoded_location = urllib.parse.quote(location)
url = f"https://wttr.in/{encoded_location}?format=j1"
urlemoji = f"https://wttr.in/{encoded_location}?format=%c"
for attempt in range(5):
try:
response = requests.get(urlemoji, timeout=3)
responseinfo = requests.get(url, timeout=3).json()
if response.status_code == 200:
temp_unit = "C"
temp = (
responseinfo["current_condition"][0][f"temp_{temp_unit}"] + "°"
)
condition = responseinfo["current_condition"][0]["weatherDesc"][0][
"value"
]
maxtemp = responseinfo["weather"][0]["maxtempC"] + "°"
mintemp = responseinfo["weather"][0]["mintempC"] + "°"
location = responseinfo["nearest_area"][0]["areaName"][0]["value"]
emoji = response.text.strip()
GLib.idle_add(
callback,
[
emoji,
temp,
condition,
location,
maxtemp,
mintemp,
],
)
return
except requests.RequestException as e:
print(f"Error fetching weather (attempt {attempt + 1}): {e}")
if attempt < 4:
time.sleep(10)
else:
GLib.idle_add(callback, None)
return
executor.submit(fetch_weather)
def update_weather(widget):
def fetch_and_update():
get_weather(lambda weather_info: update_widget(widget, weather_info))
return True
GLib.timeout_add_seconds(600, fetch_and_update)
fetch_and_update()
def update_widget(widget, weather_info):
if weather_info:
widget.weatherinfo = weather_info
widget.update_labels(weather_info)
class Weather(Box):
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.city = Label(
name="city", label="New York", justification="right", h_align="start"
)
self.temperature = Label(name="temperature", label="36", h_align="start")
self.condition_em = Label(name="condition-emoji", label="☁️", h_align="start")
self.condition = Label(name="condition", label="Cloudy", h_align="start")
self.feels_like = Label(name="feels-like", label="H:28 L:20", h_align="start")
self.add(self.city)
self.add(self.temperature)
self.add(self.condition_em)
self.add(self.condition)
self.add(self.feels_like)
update_weather(self)
def update_labels(self, weather_info):
if not self.weatherinfo:
return
# Unpack weather info into variables for better readability
emoji, temp, condition, location, maxtemp, mintemp = self.weatherinfo
maxmin = f"H:{maxtemp} L:{mintemp}"
# Store references to deeply nested children to avoid repeated lookups
self.city.set_label(location)
self.feels_like.set_label(maxmin)
self.condition.set_label(condition)
self.temperature.set_label(temp)
self.condition_em.set_label(emoji)
self.parent.set_visible(True)
class WeatherContainer(Box):
def __init__(self, **kwargs):
super().__init__(
orientation="v",
name="box-widget-2",
v_expand=True,
v_align="center",
size=(170, 170),
visible=True,
h_align="center",
children=[
Weather(self),
],
**kwargs,
)
# TODO: Background Colours according to weather condtions?
# self.set_style("background-color: rgba(0, 0, 0, 0.5);")
# self.add(Weather())
class Date(Box):
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,
)
self.top = Box(orientation="h", name="date-top", h_expand=True)
self.dateone = DateTime(formatters=["%a"], interval=10000, name="day")
self.datetwo = DateTime(formatters=["%b"], interval=10000, name="month")
self.datethree = DateTime(formatters=["%-d"], interval=10000, name="date")
self.top.add(self.dateone)
self.top.add(self.datetwo)
self.add(self.top)
self.add(self.datethree)
class DateContainer(Box):
def __init__(self, **kwargs):
super().__init__(
orientation="v",
name="box-widget",
v_expand=True,
size=(170, 170),
v_align="center",
h_align="center",
children=[Date()],
**kwargs,
)
class Calendar(Box):
def __init__(self, **kwargs):
super().__init__(
name="calendar-widget",
h_expand=True,
v_expand=True,
orientation="v",
**kwargs,
)
# Create current date for calendar
now = datetime.datetime.now()
self.current_month = now.month
self.current_year = now.year
self.current_day = now.day
# Month and year header
self.month_label = Label(
name="calendar-month",
label=f"{calendar.month_name[self.current_month]}",
h_align="start",
justification="left",
)
# Day abbreviations
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):
# Saturday (6) and Sunday (0) get weekend styling
is_weekend = i == 0 or i == 6
day_label = Label(
name=(
"calendar-day-header-weekend"
if is_weekend
else "calendar-day-header"
),
label=day_name,
h_align="center",
h_expand=True,
)
days_header.add(day_label)
# Calendar grid
self.calendar_grid = Box(name="calendar-grid", orientation="v", spacing=1)
self.update_calendar()
self.add(self.month_label)
self.add(days_header)
self.add(self.calendar_grid)
# Update calendar daily
invoke_repeater(86400000, self.update_calendar_if_needed) # 24 hours
def update_calendar_if_needed(self):
"""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.current_month = now.month
self.current_year = now.year
self.current_day = now.day
self.update_calendar()
return True
def update_calendar(self):
"""Update the calendar grid with current month."""
# Clear existing calendar
for child in self.calendar_grid.get_children():
self.calendar_grid.remove(child)
# Update month label
self.month_label.set_label(f"{calendar.month_name[self.current_month]}")
# Get calendar for current month
cal = calendar.monthcalendar(self.current_year, self.current_month)
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
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 == datetime.datetime.now().month
and self.current_year == datetime.datetime.now().year
)
# Saturday (6) and Sunday (0) get weekend styling
is_weekend = day_index == 0 or day_index == 6
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):
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 RamInfo(Box):
@staticmethod
def bake_progress_bar(name: str = "progress-bar", size: int = 80, **kwargs):
return CircularProgressBar(
name=name,
start_angle=270,
end_angle=630,
min_value=0,
max_value=100,
size=size,
**kwargs,
)
@staticmethod
def bake_progress_label(text, **kwargs):
return Label(label=text, **kwargs)
def __init__(self, **kwargs):
super().__init__(
layer="bottom",
title="sysinfo",
name="info-box-widget",
visible=True,
size=(170, 170),
h_expand=True,
v_expand=True,
all_visible=True,
**kwargs,
)
self.ram_progress = self.bake_progress_bar(name="progress")
self.ram_label = Label(
label="15%\nRam", justification="center", name="progress-label"
)
# Create info with color indicators
self.used_indicator = Label(label="■", name="used-color-indicator")
self.free_indicator = Label(label="■", name="free-color-indicator")
# Create the info labels
self.used_info = Label(label="Used", name="info-text")
self.used_value = Label(label="0.0GB", name="info-value")
self.free_info = Label(label="Free", name="info-text")
self.free_value = Label(label="0.0GB", name="info-value")
# Create horizontal boxes for each info line
self.used_line = Box(
orientation="h",
spacing=4,
h_align="start",
children=[
self.used_indicator,
self.used_info,
self.used_value,
]
)
self.free_line = Box(
orientation="h",
spacing=4,
h_align="start",
children=[
self.free_indicator,
self.free_info,
self.free_value,
]
)
self.ram_info_container = Box(
name="info-container",
orientation="v",
spacing=2,
h_align="center",
children=[
self.used_line,
self.free_line,
]
)
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.ram_progress,
tooltip_text="",
overlays=self.ram_label,
),
],
),
Box(
h_align="center",
justification="centre",
orientation="v",
children=[
self.ram_info_container,
],
),
],
)
self.add(self.progress_container)
invoke_repeater(1000, self.update)
def update(self):
mem = psutil.virtual_memory()
self.ram_label.set_label(f" {round(mem.percent):<2} %\nRAM")
used_gb = mem.used / (1024**3)
free_gb = mem.available / (1024**3)
self.used_value.set_label(f"{round(used_gb, 1)}GB")
self.free_value.set_label(f"{round(free_gb, 1)}GB")
GLib.idle_add(self.ram_progress.set_value, mem.percent)
# executor.submit(update)
return True
class CpuInfo(Box):
@staticmethod
def bake_progress_bar(name: str = "progress-bar", size: int = 80, **kwargs):
return CircularProgressBar(
name=name,
start_angle=270,
end_angle=630,
min_value=0,
max_value=100,
size=size,
**kwargs,
)
@staticmethod
def bake_progress_label(text, **kwargs):
return Label(label=text, **kwargs)
def __init__(self, **kwargs):
super().__init__(
layer="bottom",
title="sysinfo",
name="info-box-widget",
visible=True,
size=(170, 170),
h_expand=True,
v_expand=True,
all_visible=True,
**kwargs,
)
self.cpu_progress = self.bake_progress_bar(name="progress")
self.cpu_label = Label(
label="15%\nCPU", justification="center", name="progress-label"
)
# Create temp info with color indicator
self.temp_indicator = Label(label="■", name="temp-color-indicator")
self.temp_info = Label(label="Temp", name="info-text")
self.temp_value = Label(label="0°C", name="info-value")
self.temp_line = Box(
orientation="h",
spacing=4,
h_align="start",
children=[
self.temp_indicator,
self.temp_info,
self.temp_value,
]
)
self.cpu_info_container = Box(
name="info-container",
orientation="v",
h_align="center",
children=[
self.temp_line,
]
)
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.cpu_progress,
tooltip_text="",
overlays=self.cpu_label,
),
],
),
Box(
h_align="center",
justification="centre",
orientation="v",
children=[
self.cpu_info_container,
],
),
],
)
self.add(self.progress_container)
invoke_repeater(1000, self.update)
def get_cpu_temp(self):
temps = psutil.sensors_temperatures()
if not temps:
return None
for name, entries in temps.items():
if (
"coretemp" in name.lower()
or "k10temp" in name.lower()
or "cpu" in name.lower()
):
for entry in entries:
# Match common CPU temp labels
if (
"package id 0" in (entry.label or "").lower()
or "core 0" in (entry.label or "").lower()
or (entry.label is None or entry.label.strip() == "")
):
return round(entry.current, 1)
return None
def update(self):
cpu = psutil.cpu_percent()
self.cpu_label.set_label(f" {round(cpu):<2} %\nCPU")
temp = self.get_cpu_temp()
if temp is not None:
self.temp_value.set_label(f"{temp}°C")
else:
self.temp_value.set_label("N/A")
GLib.idle_add(self.cpu_progress.set_value, cpu)
# executor.submit(update)
return True
# FIX: GTK ERRORS
class Deskwidgets(Window):
config = load_config()
def __init__(self, **kwargs):
top_left = Window(
anchor="top left",
orientation="h",
layer="bottom",
child=Box(
name="desktop-widgets-container",
children=[
DateContainer(),
WeatherContainer(),
CalendarContainer(),
],
),
)
bottom_left = Window(
anchor="bottom right",
orientation="h",
layer="bottom",
child=Box(
name="desktop-widgets-container",
children=[
CpuInfo(),
RamInfo(),
],
),
)
container = Box(
orientation="v",
children=[
top_left,
bottom_left,
],
)
super().__init__(
name="desktop",
layer="bottom",
title="desktop-widgets",
orientation="v",
exclusivity="none",
child=container,
**kwargs,
)