From 2e9c647442ed3d12079954ca25653f933685756a Mon Sep 17 00:00:00 2001 From: tr1x_em Date: Sun, 3 Aug 2025 16:53:34 +0530 Subject: [PATCH] Added DateWidget --- main.css | 41 ++++ main.py | 7 +- modules/widget.py | 555 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 602 insertions(+), 1 deletion(-) create mode 100644 modules/widget.py diff --git a/main.css b/main.css index 356ab00..20ed279 100644 --- a/main.css +++ b/main.css @@ -71,3 +71,44 @@ menu > menuitem:hover { menu > menuitem:hover > label { color: var(--shadow); } + + + + + + + + + + + +#day label, +#month label{ + color: var(--foreground); + padding: 5px; + margin-top: 10px; + font-size: 30px; + color: alpha(#fff, 0.7); + font-family: "SF Pro Rounded"; + font-weight: 600; +} + +#date label{ + color: alpha(#fff, 0.7); + margin-top: -10px; + font-size: 90px; + font-weight: 600; + font-family: "SF Pro Rounded"; +} +#day label{ + color: alpha(#FF0000, 0.7); +} +#date{ +} +#box-widget{ + background-color: alpha(#232323, 1); + margin-top: 20px; + min-width: 170px; + min-height: 170px; + border-radius: 16px; +} diff --git a/main.py b/main.py index 7625575..c659ff5 100644 --- a/main.py +++ b/main.py @@ -1,3 +1,4 @@ +from fabric.core import widgets import setproctitle from loguru import logger @@ -7,6 +8,7 @@ from fabric.utils import get_relative_path, monitor_file from modules.dock import Dock from modules.launcher.main import Launcher from modules.notification.notification import ModusNoti +from modules.widget import Deskwidgets from modules.osd import OSD # from modules.corners import Corners @@ -41,6 +43,7 @@ if __name__ == "__main__": panel.launcher = launcher osd = OSD() + widgets = Deskwidgets() # Set corners visibility based on config # corners_visible = config.get("corners_visible", True) # corners.set_visible(corners_visible) @@ -50,7 +53,9 @@ if __name__ == "__main__": _ = css_file.connect("changed", lambda *_: set_css()) # Make sure corners is added to the app - app = Application(f"{APP_NAME}", panel, dock, switcher, launcher, modusnoti, osd) + app = Application( + f"{APP_NAME}", panel, dock, switcher, launcher, modusnoti, osd, widgets + ) def set_css(): app.set_stylesheet_from_file( diff --git a/modules/widget.py b/modules/widget.py new file mode 100644 index 0000000..d66a246 --- /dev/null +++ b/modules/widget.py @@ -0,0 +1,555 @@ +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 +import config.data as data +from config.data import load_config +import subprocess + +executor = ThreadPoolExecutor(max_workers=4) + + +config = load_config() + + +def margin(): + return ( + data.DOCK_ICON_SIZE + 10 + if not data.DOCK_ALWAYS_OCCLUDED and data.DOCK_ENABLED + else 0 + ) + + +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}"] + "°" + ) + feels_like = ( + responseinfo["current_condition"][0][f"FeelsLike{temp_unit}"] + + "°" + ) + condition = responseinfo["current_condition"][0]["weatherDesc"][0][ + "value" + ] + location = responseinfo["nearest_area"][0]["areaName"][0]["value"] + emoji = response.text.strip() + update_time = datetime.datetime.now().strftime("%I:%M:%S %p") + + GLib.idle_add( + callback, + [emoji, temp, condition, feels_like, location, update_time], + ) + 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 Sysinfo(Box): + @staticmethod + def bake_progress_bar(name: str = "progress-bar", size: int = 45, **kwargs): + return CircularProgressBar( + name=name, + start_angle=180, + end_angle=540, + min_value=0, + max_value=100, + size=size, + **kwargs, + ) + + @staticmethod + def bake_progress_icon(**kwargs): + return Label(**kwargs).build().add_style_class("progress-icon").unwrap() + + def __init__(self, **kwargs): + super().__init__( + layer="bottom", + title="sysinfo", + name="sysinfo", + visible=False, + all_visible=False, + **kwargs, + ) + + self.cpu_progress = self.bake_progress_bar() + self.ram_progress = self.bake_progress_bar() + self.bat_circular = self.bake_progress_bar().build().set_value(42).unwrap() + + self.progress_container = Box( + name="progress-bar-container", + spacing=12, + children=[ + Box( + children=[ + Overlay( + child=self.cpu_progress, + tooltip_text="", + overlays=[ + self.bake_progress_icon( + label="", + name="progress-icon-cpu", + ) + ], + ), + ], + ), + Box( + children=[ + Overlay( + child=self.ram_progress, + tooltip_text="", + overlays=[ + self.bake_progress_icon( + name="progress-icon-ram", + label="󰘚", + ) + ], + ) + ] + ), + Box( + children=[ + Overlay( + child=self.bat_circular, + tooltip_text="", + overlays=[ + self.bake_progress_icon( + label="󱊣", + name="progress-icon-bat", + ) + ], + ), + ], + ), + ], + ) + + self.update_status() + invoke_repeater(1000, self.update_status) + + self.add( + Box( + name="progress-bar-container-main", + orientation="v", + spacing=24, + children=[self.progress_container], + ), + ) + self.show_all() + + def update_status(self): + """Update system info asynchronously to prevent UI lag.""" + + def update(): + cpu = psutil.cpu_percent() + ram = psutil.virtual_memory().percent + battery = ( + psutil.sensors_battery().percent if psutil.sensors_battery() else 80 + ) + GLib.idle_add(self.cpu_progress.set_value, cpu) + GLib.idle_add(self.ram_progress.set_value, ram) + GLib.idle_add(self.bat_circular.set_value, battery) + + GLib.idle_add(self.cpu_progress.set_tooltip_text, f"{str(round(cpu))}%") + GLib.idle_add(self.ram_progress.set_tooltip_text, f"{str(round(ram))}%") + GLib.idle_add(self.bat_circular.set_tooltip_text, f"{str(round(battery))}%") + + executor.submit(update) + return True + + +class WeatherWidget(Box): + def __init__(self, **kwargs): + super().__init__( + layer="bottom", + name="weather-widget", + anchor="center", + h_align="center", + v_align="center", + h_expand=True, + visible=False, + v_expand=True, + **kwargs, + ) + self.weatherinfo = None + self.header = Box( + name="header1", + orientation="h", + h_expand=True, + children=[ + Box( + orientation="v", + children=[ + Label(label="", h_align="start", name="weather-location"), + Label(label="", h_align="start", name="weather-update-time"), + Label(label="", h_align="start", name="weather-condition"), + ], + ), + ], + ) + self.temp = Box( + name="header2", + orientation="h", + h_align="center", + children=[ + Box( + orientation="v", + children=[ + Label(label="", name="weather-current-temperature"), + Label(label="", name="weather-feels-like-temperature"), + ], + ), + ], + ) + self.header_right = Box( + name="header3", + orientation="h", + children=[ + Box( + orientation="v", + children=[ + Label(label="", name="weather-emoji"), + ], + ), + ], + ) + self.add( + Box( + name="window-inner", + orientation="h", + h_align="center", + v_align="center", + children=[self.header, self.temp, self.header_right], + ), + ) + self.set_visible(False) + 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, feels_like, location, update_time = self.weatherinfo + + # Store references to deeply nested children to avoid repeated lookups + header_left = self.header.children[0].children + temp_labels = self.temp.children[0].children + header_right = self.header_right.children[0].children + + # Update labels efficiently + self.set_visible(True) + header_left[0].set_label(location) + header_left[1].set_label(f"Updated {update_time}") + header_left[2].set_label(condition) + temp_labels[0].set_label(temp) + temp_labels[1].set_label(f"Feels like {feels_like}") + header_right[0].set_label(emoji) + + +def fetch_quote(callback): + """Fetch quotes asynchronously.""" + + def fetch(): + quotes_type = "stoic" + + url = ( + "https://stoic-quotes.com/api/quote" + if quotes_type == "stoic" + else "https://zenquotes.io/api/random" + ) + + for attempt in range(5): + try: + response = requests.get(url, timeout=3) + response.raise_for_status() + respdata = response.json() + quote = ( + f"{respdata[0]['q']} - {respdata[0]['a']}" + if quotes_type == "zen" + else f"{respdata['text']} - {respdata['author']}" + ) + break + except requests.RequestException as e: + print(f"Error fetching quote: {e}") + if attempt < 4: + time.sleep(10) + else: + try: + result = subprocess.run( + ["hyprctl", "splash"], + capture_output=True, + text=True, + check=True, + ) + quote = result.stdout.strip() + " - Team Hyprland" + except subprocess.CalledProcessError as e: + print(f"Error fetching quote from hyprctl: {e}") + quote = "I learn from the mistakes of people who take my advice - Trix" + + GLib.idle_add(callback, quote) + + executor.submit(fetch) + + +def fetch_quote_async(callback): + GLib.idle_add(lambda: fetch_quote(callback)) + + +class QuoteWidget(Label): + def __init__(self, **kwargs): + super().__init__( + name="quote", + label="", + anchor="center", + h_align="center", + v_align="center", + h_expand=True, + justification="center", + v_expand=True, + visible=False, + ) + fetch_quote_async(self.update_label) + + def update_label(self, quote): + """Update quote asynchronously.""" + max_width = 150 # Set the maximum width for the quote + if len(quote) > max_width: + words = quote.split() + line1, line2 = "", "" + for word in words: + if len(line1) + len(word) + 1 <= max_width: + line1 += word + " " + else: + line2 += word + " " + quote = line1.strip() + "\n" + line2.strip() + self.set_label(quote) + self.set_visible(True) + + +class ActivationMainText(Label): + def __init__(self, **kwargs): + super().__init__( + name="activation-main-text", + label="", + anchor="bottom right", + justification="left", + v_align="start", + h_align="start", + h_expand=True, + v_expand=True, + visible=False, + ) + self.set_label("Activate Linux") + + +class ActivationSubText(Label): + def __init__(self, **kwargs): + super().__init__( + name="activation-sub-text", + label="", + anchor="bottom right", + justification="left", + v_align="start", + h_align="start", + h_expand=True, + v_expand=True, + visible=False, + ) + self.set_label("Go to Settings to activate Linux") + + +def create_widgets(config): + widgets = [] + if config.get("widgets_displaytype_visible", True): + if config.get("widgets_clock_visible", True): + widgets.append( + DateTime(formatters=["%A, %d %B"], interval=10000, name="date") + ) + if config.get("widgets_date_visible", True): + widgets.append(DateTime(formatters=["%I:%M %p"], name="clock")) + if config.get("widgets_quote_visible", True): + widgets.append(QuoteWidget()) + if config.get("widgets_weatherwid_visible", True): + widgets.append(WeatherWidget()) + else: + widgets.append(DateTime(formatters=["%I:%M %p"], name="clock")) + widgets.append(DateTime(formatters=["%A. %d %B"], interval=10000, name="date")) + return widgets + + +# if data.DESKTOP_WIDGETS: +# if config.get("widgets_displaytype_visible", True): + + +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 Deskwidgets(Window): + def __init__(self, **kwargs): + config = load_config() + super().__init__( + name="desktop", + layer="bottom", + title="desktop-widgets", + exclusivity="none", + child=[ + Window( + anchor="top left", + layer="bottom", + child=[ + Box( + orientation="v", + name="box-widget", + v_expand=True, + v_align="center", + h_align="center", + children=[Date()], + ) + ], + ) + ], + all_visible=True, + **kwargs, + ) + + +# else: +# +# class Deskwidgets(Window): +# def __init__(self, **kwargs): +# config = load_config() +# super().__init__(name="desktop", **kwargs) +# desktop_widget = Window( +# layer="bottom", +# anchor="bottom left", +# v_align="start", +# h_align="start", +# h_expand=True, +# v_expand=True, +# justification="left", +# exclusivity="none", +# child=Box( +# orientation="v", +# children=create_widgets(config), +# ), +# all_visible=True, +# ) +# if config.get("widgets_activation_visible", True): +# activationnag = Window( +# name="activation", +# anchor="bottom right", +# layer="top", +# justification="left", +# v_align="start", +# h_align="start", +# h_expand=True, +# v_expand=True, +# child=Box( +# orientation="v", +# children=[ +# ActivationMainText(), +# ActivationSubText(), +# ], +# ), +# all_visible=True, +# ) +# else: +# activationnag = None +# +# else: +# +# class Deskwidgets(Window): +# def __init__(self, **kwargs): +# super().__init__(name="desktop", **kwargs) +# self.set_visible(False)