From 6f2d0bf44401faafdc9d47f5b60e68b8321ef764 Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Tue, 14 Jul 2026 18:45:52 +0545 Subject: [PATCH] chore: refactor imports and adopt new utils utilities --- .../icons/player/{Pause.svg => pause.svg} | 0 .../icons/player/{Rewind.svg => rewind.svg} | 0 src/main.py | 4 +- src/services/capslock.py | 2 +- src/services/config.py | 2 +- src/services/custom_notification.py | 9 +-- src/services/google_lens.py | 6 +- src/services/modus.py | 2 +- src/services/numlock.py | 2 +- src/services/todo.py | 5 +- src/services/wallpaper.py | 4 -- src/shared/data.py | 2 +- .../dialogs}/__init__.py | 0 src/shared/dialogs/about.py | 70 +++++++++---------- src/shared/widgets/__init__.py | 0 src/shared/window/__init__.py | 0 src/shared/window/battery_widget.py | 9 +-- ...expaned-player.css => expanded-player.css} | 0 src/styles/controlpanel/index.css | 2 +- src/utils/__init__.py | 4 +- src/utils/functions.py | 68 +++++++++++------- src/utils/{utils.py => gtk_utils.py} | 0 src/utils/icon_resolver.py | 9 +-- src/window/controlcenter/bluetooth.py | 2 +- src/window/controlcenter/expanded_player.py | 12 ++-- src/window/controlcenter/main.py | 2 +- src/window/controlcenter/nightlight.py | 2 +- src/window/controlcenter/per_app_volume.py | 9 +-- src/window/controlcenter/player.py | 8 +-- src/window/controlcenter/wifi.py | 2 +- src/window/desktop/widget.py | 8 +-- src/window/dock/canvas.py | 10 ++- src/window/dock/constants.py | 2 +- src/window/globalmenu/__init__.py | 0 src/{ => window}/globalmenu/dbusmenu.py | 0 src/{ => window}/globalmenu/detection.py | 10 +-- src/{ => window}/globalmenu/environment.py | 0 src/{ => window}/globalmenu/gtkmenu.py | 2 +- src/{ => window}/globalmenu/launch.py | 4 +- .../globalmenu/libmenu_button_shim.c | 0 src/{ => window}/globalmenu/service.py | 6 +- src/window/lock.py | 2 +- .../notification/notification_center.py | 16 ++--- src/window/osd/components/audio.py | 2 +- src/window/osd/components/brightness.py | 2 +- src/window/panel/__init__.py | 0 src/window/panel/components/custom_mods.py | 8 +-- src/window/panel/components/globalmenu.py | 4 +- src/window/panel/components/indicators.py | 13 ++-- src/window/panel/components/pomodoro.py | 16 +++-- src/window/panel/components/todo.py | 8 +-- src/window/panel/components/workspace.py | 2 +- src/window/panel/main.py | 2 +- src/window/panel/notch/indicators.py | 2 +- src/window/panel/notch/player.py | 2 +- src/window/panel/notch/recording.py | 2 +- src/window/screencapture/tool_button.py | 2 +- src/window/settings/__init__.py | 0 src/window/settings/main.py | 2 +- src/window/spotlight/__init__.py | 0 src/window/spotlight/core/loader.py | 66 ++++++----------- src/window/spotlight/core/manager.py | 12 ++-- src/window/spotlight/plugins/application.py | 2 +- src/window/spotlight/plugins/caffeine.py | 41 ++++------- src/window/spotlight/plugins/emoji.py | 2 +- 65 files changed, 221 insertions(+), 266 deletions(-) rename src/assets/icons/player/{Pause.svg => pause.svg} (100%) rename src/assets/icons/player/{Rewind.svg => rewind.svg} (100%) rename src/{globalmenu => shared/dialogs}/__init__.py (100%) create mode 100644 src/shared/widgets/__init__.py create mode 100644 src/shared/window/__init__.py rename src/styles/controlpanel/{expaned-player.css => expanded-player.css} (100%) rename src/utils/{utils.py => gtk_utils.py} (100%) create mode 100644 src/window/globalmenu/__init__.py rename src/{ => window}/globalmenu/dbusmenu.py (100%) rename src/{ => window}/globalmenu/detection.py (96%) rename src/{ => window}/globalmenu/environment.py (100%) rename src/{ => window}/globalmenu/gtkmenu.py (99%) rename src/{ => window}/globalmenu/launch.py (95%) rename src/{ => window}/globalmenu/libmenu_button_shim.c (100%) rename src/{ => window}/globalmenu/service.py (99%) create mode 100644 src/window/panel/__init__.py create mode 100644 src/window/settings/__init__.py create mode 100644 src/window/spotlight/__init__.py diff --git a/src/assets/icons/player/Pause.svg b/src/assets/icons/player/pause.svg similarity index 100% rename from src/assets/icons/player/Pause.svg rename to src/assets/icons/player/pause.svg diff --git a/src/assets/icons/player/Rewind.svg b/src/assets/icons/player/rewind.svg similarity index 100% rename from src/assets/icons/player/Rewind.svg rename to src/assets/icons/player/rewind.svg diff --git a/src/main.py b/src/main.py index 9934001..81e917e 100644 --- a/src/main.py +++ b/src/main.py @@ -23,7 +23,7 @@ for log in [ "services", "window", "utils", - "globalmenu", + "window.globalmenu", ]: logger.disable(log) @@ -45,7 +45,7 @@ def main(): # Generate colors.css if it doesn't exist colors_css_path = get_relative_path("styles/colors.css") if not os.path.exists(colors_css_path): - from utils.utils import generate_colors_from_wallpaper + from utils.gtk_utils import generate_colors_from_wallpaper default_wallpaper = get_relative_path("assets/wallpaper_example/example-1.png") if os.path.exists(default_wallpaper): diff --git a/src/services/capslock.py b/src/services/capslock.py index 90fb6af..1d72345 100644 --- a/src/services/capslock.py +++ b/src/services/capslock.py @@ -3,7 +3,7 @@ from pathlib import Path from fabric.core.service import Property, Service, Signal from fabric.utils import logger -from utils.utils import EvdevLEDMonitor +from utils.gtk_utils import EvdevLEDMonitor # Discover CapsLock LED device capslock_leds = list(Path("/sys/class/leds").glob("input*::capslock")) diff --git a/src/services/config.py b/src/services/config.py index 4717cdf..da37bf1 100644 --- a/src/services/config.py +++ b/src/services/config.py @@ -15,7 +15,7 @@ from tomlkit import dump as toml_dump from tomlkit import load as toml_load from tomlkit.items import Array, Bool, Integer, String -from utils.utils import toml_file +from utils.gtk_utils import toml_file class ConfigService: diff --git a/src/services/custom_notification.py b/src/services/custom_notification.py index f88d250..8a620ab 100644 --- a/src/services/custom_notification.py +++ b/src/services/custom_notification.py @@ -271,12 +271,9 @@ class CachedNotifications(Notifications): def load_cached_notifications(self) -> dict[int, CachedNotification]: """Load cached notifications from a JSON file (deserialization).""" - try: - with open(NOTIFICATION_CACHE_FILE) as file: - data = json.load(file) # Load list of serialized notifications - except (FileNotFoundError, json.JSONDecodeError): - # If file doesn't exist or is corrupted, start with empty list - data = [] + from utils.functions import read_json_file + + data = read_json_file(NOTIFICATION_CACHE_FILE) or [] max_cache_id = 0 for notification in data: diff --git a/src/services/google_lens.py b/src/services/google_lens.py index 8e7cbe2..a2be602 100644 --- a/src/services/google_lens.py +++ b/src/services/google_lens.py @@ -31,11 +31,13 @@ class GoogleLens(Service): super().__init__(**kwargs) def capture_region(self, dest_path): + from utils.functions import find_binary + # Check for dependencies - if run_command(["which", "grim"]).returncode != 0: + if not find_binary("grim"): raise RuntimeError("grim not found - please install it") - if run_command(["which", "slurp"]).returncode != 0: + if not find_binary("slurp"): raise RuntimeError("slurp not found - please install it") try: diff --git a/src/services/modus.py b/src/services/modus.py index f8adb3a..e23c6be 100644 --- a/src/services/modus.py +++ b/src/services/modus.py @@ -561,7 +561,7 @@ def close_window(address: str): def launch_app(command_line: str): - from globalmenu.launch import launch_command + from window.globalmenu.launch import launch_command launch_command(command_line) diff --git a/src/services/numlock.py b/src/services/numlock.py index f24261d..0f92017 100644 --- a/src/services/numlock.py +++ b/src/services/numlock.py @@ -3,7 +3,7 @@ from pathlib import Path from fabric.core.service import Property, Service, Signal from fabric.utils import logger -from utils.utils import EvdevLEDMonitor +from utils.gtk_utils import EvdevLEDMonitor # Discover NumLock LED device numlock_leds = list(Path("/sys/class/leds").glob("input*::numlock")) diff --git a/src/services/todo.py b/src/services/todo.py index 612bf82..93e622b 100644 --- a/src/services/todo.py +++ b/src/services/todo.py @@ -47,10 +47,11 @@ class TodoService(Service): def _load_todos(self): """Load todos from JSON file""" + from utils.functions import read_json_file + try: if self._file_path.exists(): - with open(self._file_path, encoding="utf-8") as f: - self._todos = json.load(f) + self._todos = read_json_file(str(self._file_path)) or [] else: self._todos = [] except Exception as e: diff --git a/src/services/wallpaper.py b/src/services/wallpaper.py index 1e0893d..e7653c8 100644 --- a/src/services/wallpaper.py +++ b/src/services/wallpaper.py @@ -51,10 +51,6 @@ def create_thumbnail( return False -def generate_colors_from_wallpaper(image_path: str) -> bool: - return bool(exec_shell_command_async(f'matugen image "{image_path}"')) - - class WallpaperService(Service): @Signal def wallpaper_ready(self, image_path: str, thumbnail_path: str) -> None: ... diff --git a/src/shared/data.py b/src/shared/data.py index d24935d..61312ea 100644 --- a/src/shared/data.py +++ b/src/shared/data.py @@ -1,7 +1,7 @@ from fabric.utils import GLib, get_relative_path, logger, os from utils.functions import parse_timeout_string -from utils.utils import toml_file +from utils.gtk_utils import toml_file HOME_DIR = GLib.get_home_dir() APP_NAME = "modus" diff --git a/src/globalmenu/__init__.py b/src/shared/dialogs/__init__.py similarity index 100% rename from src/globalmenu/__init__.py rename to src/shared/dialogs/__init__.py diff --git a/src/shared/dialogs/about.py b/src/shared/dialogs/about.py index 135c005..7ff3d0b 100644 --- a/src/shared/dialogs/about.py +++ b/src/shared/dialogs/about.py @@ -1,10 +1,8 @@ -import subprocess - from fabric.utils import GdkPixbuf, Gtk, get_relative_path, logger, os, re -from utils.functions import escape_markup_text, find_binary +from utils.functions import escape_markup_text, find_binary, run_command +from utils.gtk_utils import setup_cursor_hover from utils.icon_resolver import IconResolver -from utils.utils import setup_cursor_hover def read_dmi(field): @@ -20,13 +18,14 @@ def read_dmi(field): def get_gpu_name(): try: - output = ( - subprocess.check_output( - "lspci -nn | grep -i 'VGA compatible controller'", shell=True, text=True - ) - .strip() - .split("\n") - ) + result = run_command(["lspci", "-nn"]) + if result.returncode != 0: + return "Unknown" + output = [ + line + for line in result.stdout.strip().split("\n") + if "VGA compatible controller" in line.lower() + ] def clean(line): matches = re.findall(r"\[(.*?)\]", line) @@ -495,37 +494,38 @@ class About(Gtk.Window): return label # Info values + kernel_result = run_command(["uname", "-r"]) + cpu_result = run_command(["lscpu"]) + cpu_model = "" + if cpu_result.returncode == 0: + for line in cpu_result.stdout.split("\n"): + if "Model name:" in line: + cpu_model = line.split(":", 1)[1].strip() + break + mem_result = run_command(["free", "-h", "--giga"]) + mem_value = "" + if mem_result.returncode == 0: + for line in mem_result.stdout.split("\n"): + if line.startswith("Mem:"): + mem_value = line.split()[1] + break + uptime_result = run_command(["uptime", "-p"]) + 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(), + kernel_result.stdout.strip() + if kernel_result.returncode == 0 + else "Unknown", ), + ("CPU", cpu_model), + ("Memory", mem_value), ("GPU", get_gpu_name()), ( "Uptime", - subprocess.run( - "uptime -p", shell=True, capture_output=True, text=True - ).stdout.strip(), + uptime_result.stdout.strip() + if uptime_result.returncode == 0 + else "Unknown", ), ] diff --git a/src/shared/widgets/__init__.py b/src/shared/widgets/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/shared/window/__init__.py b/src/shared/window/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/shared/window/battery_widget.py b/src/shared/window/battery_widget.py index ccda227..c864c42 100644 --- a/src/shared/window/battery_widget.py +++ b/src/shared/window/battery_widget.py @@ -9,7 +9,7 @@ from fabric.widgets.separator import Separator from services.battery import Battery from services.gamemode import GameModeService from utils.functions import clear_children, format_duration -from utils.utils import svg_file +from utils.gtk_utils import svg_file class EnergyModeButton(Box): @@ -332,9 +332,6 @@ class BatteryControl(Box): self.game_mode_button = GameModeButton(parent=self) self.game_mode_container.add(self.game_mode_button) - def _format_time(self, seconds: int) -> str: - return format_duration(seconds) - def update_battery_info(self): is_present = self.battery_service.available if not is_present: @@ -360,7 +357,7 @@ class BatteryControl(Box): if state in ["CHARGING", "PENDING_CHARGE"]: self.power_source_label.set_label("Power Source: Power Adapter") seconds_to_full = self.battery_service.time_to_full - time_to_full = self._format_time(seconds_to_full) + time_to_full = format_duration(seconds_to_full) if time_to_full != "N/A" and time_to_full != "0m": self.charging_time_label.set_label( f"{time_to_full} until fully charged" @@ -373,7 +370,7 @@ class BatteryControl(Box): elif state in ["DISCHARGING", "PENDING_DISCHARGE"]: self.power_source_label.set_label("Power Source: Battery") seconds_to_empty = self.battery_service.time_remaining - time_to_empty = self._format_time(seconds_to_empty) + time_to_empty = format_duration(seconds_to_empty) if time_to_empty != "N/A" and not time_to_empty.startswith( "4553h" ): # Filter out unrealistic times diff --git a/src/styles/controlpanel/expaned-player.css b/src/styles/controlpanel/expanded-player.css similarity index 100% rename from src/styles/controlpanel/expaned-player.css rename to src/styles/controlpanel/expanded-player.css diff --git a/src/styles/controlpanel/index.css b/src/styles/controlpanel/index.css index 7844da5..ba43d04 100644 --- a/src/styles/controlpanel/index.css +++ b/src/styles/controlpanel/index.css @@ -4,5 +4,5 @@ @import url("controlpanel/sliders.css"); @import url("controlpanel/per_app_volume.css"); @import url("controlpanel/player.css"); -@import url("controlpanel/expaned-player.css"); +@import url("controlpanel/expanded-player.css"); @import url("controlpanel/networks.css"); diff --git a/src/utils/__init__.py b/src/utils/__init__.py index 3f03f12..6301b5d 100644 --- a/src/utils/__init__.py +++ b/src/utils/__init__.py @@ -1,4 +1,4 @@ """ -Modus services package. -Contains background services and utilities for the shell. +Modus utilities package. +Contains helper functions and utility modules for the shell. """ diff --git a/src/utils/functions.py b/src/utils/functions.py index 29d4bc8..20f7467 100644 --- a/src/utils/functions.py +++ b/src/utils/functions.py @@ -2,6 +2,7 @@ import ctypes import html import json import os +import re import subprocess import threading from collections.abc import Callable @@ -67,37 +68,54 @@ def set_process_name(name: str): libc.prctl(15, name.encode("utf-8"), 0, 0, 0) # 15 = PR_SET_NAME -def parse_timeout_string(timeout_str): +_DURATION_RE = re.compile( + r"^\s*(\d+)\s*(s|sec|secs|m|min|mins|h|hr|hrs|hour|hours)?\s*$", + re.IGNORECASE, +) +_UNIT_SECS = { + "s": 1, + "sec": 1, + "secs": 1, + "m": 60, + "min": 60, + "mins": 60, + "h": 3600, + "hr": 3600, + "hrs": 3600, + "hour": 3600, + "hours": 3600, +} + + +def parse_timeout_string(timeout_str, in_seconds=False): """ - Parse timeout string in format like '5s', '10m', '30s' etc. - Returns timeout in milliseconds. + Parse timeout string in format like '5s', '10m', '2hr', '1hour' etc. + + Args: + timeout_str: String to parse (e.g., "5s", "10m", "2hr", "1hour") + in_seconds: If True, return seconds; if False, return milliseconds + + Returns: + Parsed time value (milliseconds by default, seconds if in_seconds=True) + Returns 5000 (ms) or 5 (s) as default on parse failure. """ if not timeout_str or not isinstance(timeout_str, str): - return 5000 + return 5 if in_seconds else 5000 - timeout_str = timeout_str.strip().lower() + m = _DURATION_RE.match(timeout_str.strip()) + if not m: + logger.warning(f"[functions] Could not parse timeout string: {timeout_str}") + return 5 if in_seconds else 5000 - if timeout_str.endswith("s"): - try: - seconds = int(timeout_str[:-1]) - return seconds * 1000 - except ValueError as e: - logger.warning(f"[functions] seconds = int(timeout_str[:-1]) failed: {e}") - return 5000 - elif timeout_str.endswith("m"): - try: - minutes = int(timeout_str[:-1]) - return minutes * 60 * 1000 - except ValueError as e: - logger.warning(f"[functions] minutes = int(timeout_str[:-1]) failed: {e}") - return 5000 + value = int(m.group(1)) + unit = m.group(2) # None means bare number (seconds) + + if unit is None: + seconds = value else: - try: - seconds = int(timeout_str) - return seconds * 1000 - except ValueError as e: - logger.warning(f"[functions] seconds = int(timeout_str) failed: {e}") - return 5000 + seconds = value * _UNIT_SECS.get(unit.lower(), 1) + + return seconds if in_seconds else seconds * 1000 # Threading helper functions diff --git a/src/utils/utils.py b/src/utils/gtk_utils.py similarity index 100% rename from src/utils/utils.py rename to src/utils/gtk_utils.py diff --git a/src/utils/icon_resolver.py b/src/utils/icon_resolver.py index 811b150..977b17e 100644 --- a/src/utils/icon_resolver.py +++ b/src/utils/icon_resolver.py @@ -14,13 +14,10 @@ class IconResolver: def __init__( self, default_application_icon: str = "application-x-executable-symbolic" ): + from utils.functions import read_json_file + if os.path.exists(ICON_CACHE_FILE): - with open(ICON_CACHE_FILE) as f: - try: - self._icon_dict = json.load(f) - except json.JSONDecodeError: - logger.info("[ICONS] Cache file does not exist or is corrupted") - self._icon_dict = {} + self._icon_dict = read_json_file(ICON_CACHE_FILE) or {} else: self._icon_dict = {} diff --git a/src/window/controlcenter/bluetooth.py b/src/window/controlcenter/bluetooth.py index 1180460..d3f2bb0 100644 --- a/src/window/controlcenter/bluetooth.py +++ b/src/window/controlcenter/bluetooth.py @@ -12,7 +12,7 @@ from services.bluetooth import BluetoothClient, BluetoothDevice from shared.widgets.smooth_switch import SmoothSwitch from shared.window.animated_scrollwindow import AnimatedScrollable from utils.functions import spawn_detached -from utils.utils import svg_file +from utils.gtk_utils import svg_file def get_battery_icon_file( diff --git a/src/window/controlcenter/expanded_player.py b/src/window/controlcenter/expanded_player.py index fec01ec..4b77671 100644 --- a/src/window/controlcenter/expanded_player.py +++ b/src/window/controlcenter/expanded_player.py @@ -11,7 +11,7 @@ from fabric.widgets.stack import Stack from fabric.widgets.wayland import WaylandWindow as Window from services.mpris import PlayerManager, PlayerService -from utils.utils import svg_file +from utils.gtk_utils import svg_file from window.controlcenter.player import ( PLAYER_FALLBACK_ART, apply_player_art, @@ -416,7 +416,7 @@ class PlayerBox(Box): initial_status = str(self.player.playback_status).lower() icon_file = ( - "player/play.svg" if initial_status == "paused" else "player/Pause.svg" + "player/play.svg" if initial_status == "paused" else "player/pause.svg" ) self.play_pause_icon = svg_file(icon_file, size=22) self.play_pause_button = Button( @@ -433,7 +433,7 @@ class PlayerBox(Box): ) self.prev_button = Button( name="macos-control-button", - child=svg_file("player/Rewind.svg", size=22), + child=svg_file("player/rewind.svg", size=22), style_classes=["control-buttons"], on_clicked=self._on_player_prev, ) @@ -638,7 +638,7 @@ class PlayerBox(Box): try: status = str(self.player.playback_status).lower() self.play_pause_icon.dynamic_file( - "player/play.svg" if status == "paused" else "player/Pause.svg" + "player/play.svg" if status == "paused" else "player/pause.svg" ) except Exception as e: logger.error(f"An error occurred: {e}") @@ -651,7 +651,7 @@ class PlayerBox(Box): self._last_notified_status = current_status icon_file = ( - "player/play.svg" if current_status == "paused" else "player/Pause.svg" + "player/play.svg" if current_status == "paused" else "player/pause.svg" ) if hasattr(self, "play_pause_icon"): self.play_pause_icon.dynamic_file(icon_file) @@ -659,7 +659,7 @@ class PlayerBox(Box): # Fallback if play_pause_icon isn't available import os - from utils.utils import get_relative_path + from utils.gtk_utils import get_relative_path self.play_pause_button.get_child().set_from_file( os.path.join(get_relative_path("assets/icons/"), icon_file) diff --git a/src/window/controlcenter/main.py b/src/window/controlcenter/main.py index 0acf95d..4cf9cd7 100644 --- a/src/window/controlcenter/main.py +++ b/src/window/controlcenter/main.py @@ -11,8 +11,8 @@ from services.inhibit import get_inhibit_service from services.network import NetworkClient from shared.widgets.flat_scale import FlatScale from shared.window.applet_window import AppletWindow +from utils.gtk_utils import svg_file from utils.roam import audio_service, modus_service -from utils.utils import svg_file from window.controlcenter.bluetooth import ( BluetoothConnections, set_bluetooth_enabled_with_fallback, diff --git a/src/window/controlcenter/nightlight.py b/src/window/controlcenter/nightlight.py index f041cef..cfa93af 100644 --- a/src/window/controlcenter/nightlight.py +++ b/src/window/controlcenter/nightlight.py @@ -6,7 +6,7 @@ from services.modus import ( is_night_light_active, toggle_night_light, ) -from utils.utils import svg_file +from utils.gtk_utils import svg_file def create_night_light_widget(control_center): diff --git a/src/window/controlcenter/per_app_volume.py b/src/window/controlcenter/per_app_volume.py index d9e270d..60a2afb 100644 --- a/src/window/controlcenter/per_app_volume.py +++ b/src/window/controlcenter/per_app_volume.py @@ -193,12 +193,9 @@ class PerAppVolumeControl(Box): def _populate_apps(self): """Populate the widget with current audio applications""" - # Clear and DESTROY existing widgets to prevent memory leaks - for child in list(self.apps_container.get_children()): - try: - child.destroy() - except Exception as e: - logger.error(f"An error occurred: {e}") + from utils.functions import clear_children + + clear_children(self.apps_container) self.apps_container.children = [] self._app_widgets.clear() diff --git a/src/window/controlcenter/player.py b/src/window/controlcenter/player.py index eba416f..9be2122 100644 --- a/src/window/controlcenter/player.py +++ b/src/window/controlcenter/player.py @@ -9,7 +9,7 @@ from fabric.widgets.stack import Stack import shared.data as data from services.mpris import PlayerManager, PlayerService -from utils.utils import svg_file +from utils.gtk_utils import svg_file CACHE_DIR = f"{data.CACHE_DIR}/media" PLAYER_FALLBACK_ART = get_relative_path("../../assets/icons/music.svg") @@ -357,7 +357,7 @@ class PlayerBox(Box): initial_status = str(self.player.playback_status).lower() icon_file = ( - "player/play.svg" if initial_status == "paused" else "player/Pause.svg" + "player/play.svg" if initial_status == "paused" else "player/pause.svg" ) self.play_pause_icon = svg_file(icon_file, size=22) self.play_pause_button = Button( @@ -538,7 +538,7 @@ class PlayerBox(Box): try: status = str(self.player.playback_status).lower() self.play_pause_icon.dynamic_file( - "player/play.svg" if status == "paused" else "player/Pause.svg" + "player/play.svg" if status == "paused" else "player/pause.svg" ) except Exception as e: logger.error(f"An error occurred: {e}") @@ -553,7 +553,7 @@ class PlayerBox(Box): self._last_notified_status = current_status self.play_pause_icon.dynamic_file( - "player/play.svg" if current_status == "paused" else "player/Pause.svg" + "player/play.svg" if current_status == "paused" else "player/pause.svg" ) if self.player_stack: self.player_stack.on_player_playback_changed(self, current_status) diff --git a/src/window/controlcenter/wifi.py b/src/window/controlcenter/wifi.py index 5a4ff9d..28e9bb7 100644 --- a/src/window/controlcenter/wifi.py +++ b/src/window/controlcenter/wifi.py @@ -15,7 +15,7 @@ from utils.functions import ( get_wifi_icon_for_strength, spawn_detached, ) -from utils.utils import svg_file +from utils.gtk_utils import svg_file class WifiNetworkSlot(Box): diff --git a/src/window/desktop/widget.py b/src/window/desktop/widget.py index 73aa3d9..93732db 100644 --- a/src/window/desktop/widget.py +++ b/src/window/desktop/widget.py @@ -16,7 +16,7 @@ from fabric.widgets.overlay import Overlay from fabric.widgets.wayland import WaylandWindow as Window from shared.data import load_config -from utils.utils import svg_file +from utils.gtk_utils import svg_file from window.desktop.constants import ( CALENDAR_UPDATE_INTERVAL, LOCATION_APIS, @@ -465,9 +465,9 @@ class Calendar(Box): return True def update_calendar(self): - for child in self.calendar_grid.get_children(): - self.calendar_grid.remove(child) - child.destroy() + from utils.functions import clear_children + + clear_children(self.calendar_grid) self.month_label.set_label(calendar.month_name[self.current_month]) cal = calendar.monthcalendar(self.current_year, self.current_month) for week in cal: diff --git a/src/window/dock/canvas.py b/src/window/dock/canvas.py index b53a966..212d21e 100644 --- a/src/window/dock/canvas.py +++ b/src/window/dock/canvas.py @@ -23,9 +23,9 @@ from services.modus import ( launch_app, open_trash, ) -from utils.functions import is_special_workspace_id +from utils.functions import clear_children, is_special_workspace_id +from utils.gtk_utils import svg_file from utils.icon_resolver import IconResolver -from utils.utils import svg_file from .animator import DockAnimator from .constants import ( @@ -779,9 +779,7 @@ class DockCanvas(Gtk.DrawingArea): client=None, instance_address: Optional[str] = None, ) -> None: - for child in self.menu.get_children(): - self.menu.remove(child) - child.destroy() + clear_children(self.menu) if instance_address: close_item = Gtk.MenuItem(label="Close") @@ -929,7 +927,7 @@ class DockCanvas(Gtk.DrawingArea): if command_line: launch_app(command_line) elif hasattr(app_info, "launch"): - from globalmenu.launch import launch_desktop_app + from window.globalmenu.launch import launch_desktop_app launch_desktop_app(app_info) else: diff --git a/src/window/dock/constants.py b/src/window/dock/constants.py index bd96f89..39343d7 100644 --- a/src/window/dock/constants.py +++ b/src/window/dock/constants.py @@ -1,4 +1,4 @@ -from utils.utils import toml_file +from utils.gtk_utils import toml_file PINNED_APPS_FILE = toml_file("dock.toml") diff --git a/src/window/globalmenu/__init__.py b/src/window/globalmenu/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/globalmenu/dbusmenu.py b/src/window/globalmenu/dbusmenu.py similarity index 100% rename from src/globalmenu/dbusmenu.py rename to src/window/globalmenu/dbusmenu.py diff --git a/src/globalmenu/detection.py b/src/window/globalmenu/detection.py similarity index 96% rename from src/globalmenu/detection.py rename to src/window/globalmenu/detection.py index 9ad3d8b..f608771 100644 --- a/src/globalmenu/detection.py +++ b/src/window/globalmenu/detection.py @@ -179,13 +179,9 @@ def _transitive_needed(root: Path) -> set[str]: def _load_cache() -> dict: - try: - return json.loads(_CACHE_FILE.read_text()) - except (OSError, json.JSONDecodeError) as e: - logger.warning( - f"[detection] return json.loads(_CACHE_FILE.read_text()) failed: {e}" - ) - return {} + from utils.functions import read_json_file + + return read_json_file(str(_CACHE_FILE)) or {} def _save_cache(cache: dict) -> None: diff --git a/src/globalmenu/environment.py b/src/window/globalmenu/environment.py similarity index 100% rename from src/globalmenu/environment.py rename to src/window/globalmenu/environment.py diff --git a/src/globalmenu/gtkmenu.py b/src/window/globalmenu/gtkmenu.py similarity index 99% rename from src/globalmenu/gtkmenu.py rename to src/window/globalmenu/gtkmenu.py index 41cd3b6..d2e34e3 100644 --- a/src/globalmenu/gtkmenu.py +++ b/src/window/globalmenu/gtkmenu.py @@ -2,7 +2,7 @@ from typing import Any, Dict, List, Optional from fabric.utils import Gio, GLib, logger -from globalmenu.dbusmenu import DBusMenuItem, _get_bus +from window.globalmenu.dbusmenu import DBusMenuItem, _get_bus def atspi_activate_menu_item(pid: int, label: str) -> bool: diff --git a/src/globalmenu/launch.py b/src/window/globalmenu/launch.py similarity index 95% rename from src/globalmenu/launch.py rename to src/window/globalmenu/launch.py index 4bad620..8d8fbb1 100644 --- a/src/globalmenu/launch.py +++ b/src/window/globalmenu/launch.py @@ -21,8 +21,8 @@ import re from fabric.utils import exec_shell_command_async, logger -from globalmenu.detection import executable_gtk_class -from globalmenu.environment import get_compiled_shim +from window.globalmenu.detection import executable_gtk_class +from window.globalmenu.environment import get_compiled_shim _PERCENT_RE = re.compile(r"%\w+") diff --git a/src/globalmenu/libmenu_button_shim.c b/src/window/globalmenu/libmenu_button_shim.c similarity index 100% rename from src/globalmenu/libmenu_button_shim.c rename to src/window/globalmenu/libmenu_button_shim.c diff --git a/src/globalmenu/service.py b/src/window/globalmenu/service.py similarity index 99% rename from src/globalmenu/service.py rename to src/window/globalmenu/service.py index cd9f429..b0b6b7d 100644 --- a/src/globalmenu/service.py +++ b/src/window/globalmenu/service.py @@ -17,9 +17,9 @@ from fabric.utils import ( os, ) -from globalmenu.dbusmenu import DBusMenuClient, DBusMenuItem, _get_bus -from globalmenu.environment import setup_global_menu_environment -from globalmenu.gtkmenu import ActionMenuClient, GtkMenuClient +from window.globalmenu.dbusmenu import DBusMenuClient, DBusMenuItem, _get_bus +from window.globalmenu.environment import setup_global_menu_environment +from window.globalmenu.gtkmenu import ActionMenuClient, GtkMenuClient _NODE_NAME_RE = re.compile(r' str: - return format_duration(seconds) - def _get_battery_icon_file(self, percentage: int, is_charging: bool) -> str: clamped = max(0, min(100, percentage)) step = (clamped // 10) * 10 @@ -406,11 +403,11 @@ class BatteryIndicator(Box): battery_state = f"{state_str}:{percentage}%" if state_str == "discharging": - time_to_empty = self._format_time(self._get_time_to_empty()) + time_to_empty = format_duration(self._get_time_to_empty()) if time_to_empty != "N/A": battery_state += f":{time_to_empty}" elif state_str == "charging": - time_to_full = self._format_time(self._get_time_to_full()) + time_to_full = format_duration(self._get_time_to_full()) if time_to_full != "N/A": battery_state += f":{time_to_full}" @@ -421,11 +418,11 @@ class BatteryIndicator(Box): if state == "CHARGING": tooltip += " (Charging)" - time_to_full = self._format_time(self._get_time_to_full()) + time_to_full = format_duration(self._get_time_to_full()) if time_to_full != "N/A": tooltip += f" - {time_to_full} until full" elif state == "DISCHARGING": - time_to_empty = self._format_time(self._get_time_to_empty()) + time_to_empty = format_duration(self._get_time_to_empty()) if time_to_empty != "N/A": tooltip += f" - {time_to_empty} remaining" elif state == "FULLY_CHARGED": diff --git a/src/window/panel/components/pomodoro.py b/src/window/panel/components/pomodoro.py index 571d753..d637e53 100644 --- a/src/window/panel/components/pomodoro.py +++ b/src/window/panel/components/pomodoro.py @@ -2,7 +2,7 @@ import os from enum import Enum from pathlib import Path -from fabric.utils import GLib, Gtk +from fabric.utils import GLib, Gtk, exec_shell_command_async from fabric.widgets.box import Box from fabric.widgets.button import Button from fabric.widgets.circularprogressbar import CircularProgressBar @@ -367,14 +367,14 @@ class Pomodoro(AppletWindow): if self.current_cycle >= self.max_cycles: self.state = PomodoroState.LONG_BREAK self.total_time = self.long_break_mins * 60 - os.system( - 'notify-send "Pomodoro" "Focus session complete! Time for a long break." &' + exec_shell_command_async( + 'notify-send "Pomodoro" "Focus session complete! Time for a long break."' ) else: self.state = PomodoroState.SHORT_BREAK self.total_time = self.short_break_mins * 60 - os.system( - 'notify-send "Pomodoro" "Focus session complete! Time for a short break." &' + exec_shell_command_async( + 'notify-send "Pomodoro" "Focus session complete! Time for a short break."' ) else: if self.state == PomodoroState.LONG_BREAK: @@ -384,7 +384,9 @@ class Pomodoro(AppletWindow): self.state = PomodoroState.FOCUS self.total_time = self.focus_mins * 60 - os.system('notify-send "Pomodoro" "Break over! Time to focus." &') + exec_shell_command_async( + 'notify-send "Pomodoro" "Break over! Time to focus."' + ) self.play_sound() self.time_left = self.total_time @@ -415,7 +417,7 @@ class Pomodoro(AppletWindow): if os.path.exists(sound_file): vol = float(self.volume) / 100.0 - os.system(f"pw-play --volume={vol} '{sound_file}' &") + exec_shell_command_async(f"pw-play --volume={vol} '{sound_file}'") def _update_display(self): mins = self.time_left // 60 diff --git a/src/window/panel/components/todo.py b/src/window/panel/components/todo.py index 09ac7af..14dbfc7 100644 --- a/src/window/panel/components/todo.py +++ b/src/window/panel/components/todo.py @@ -9,7 +9,7 @@ from fabric.widgets.label import Label from services.todo import get_todo_service from shared.window.animated_scrollwindow import AnimatedScrollable from shared.window.applet_window import AppletWindow -from utils.utils import svg_file +from utils.gtk_utils import svg_file class TodoItem(Box): @@ -379,10 +379,10 @@ class TodoListWidget(AppletWindow): def _refresh_todos(self, *_): """Refresh the entire todo list""" - # Clear existing items + from utils.functions import clear_children + self.todo_items.clear() - for child in self.todos_container.get_children(): - self.todos_container.remove(child) + clear_children(self.todos_container) # Get all todos todos = get_todo_service().todos diff --git a/src/window/panel/components/workspace.py b/src/window/panel/components/workspace.py index 69d0bce..74b07cc 100644 --- a/src/window/panel/components/workspace.py +++ b/src/window/panel/components/workspace.py @@ -4,7 +4,7 @@ from fabric.widgets.box import Box from services.config import get_config, on_config_change from utils.functions import is_special_workspace_id -from utils.utils import setup_cursor_hover +from utils.gtk_utils import setup_cursor_hover # TODO: Support multi-monitor setups diff --git a/src/window/panel/main.py b/src/window/panel/main.py index 9def74a..2cb49f9 100644 --- a/src/window/panel/main.py +++ b/src/window/panel/main.py @@ -9,8 +9,8 @@ from fabric.widgets.wayland import WaylandWindow as Window from services.config import get_config_all, on_config_change from services.modus import notification_service +from utils.gtk_utils import setup_cursor_hover, svg_file from utils.roam import modus_service -from utils.utils import setup_cursor_hover, svg_file from window.controlcenter.main import ModusControlCenter from window.notification.notification_center import NotificationCenter from window.panel.components.custom_mods import CustomMods diff --git a/src/window/panel/notch/indicators.py b/src/window/panel/notch/indicators.py index 1d3fdde..42e878d 100644 --- a/src/window/panel/notch/indicators.py +++ b/src/window/panel/notch/indicators.py @@ -8,7 +8,7 @@ from services.battery import Battery from services.capslock import CapsLock from services.keyboard_layout import KeyboardLayout from services.numlock import NumLock -from utils.utils import svg_file +from utils.gtk_utils import svg_file DISPLAY_MS = 2000 # how long each indicator stays visible (ms) diff --git a/src/window/panel/notch/player.py b/src/window/panel/notch/player.py index 805fb1b..6f056b4 100644 --- a/src/window/panel/notch/player.py +++ b/src/window/panel/notch/player.py @@ -8,7 +8,7 @@ from fabric.widgets.centerbox import CenterBox from services.mpris import PlayerService from shared.widgets.custom_image import CustomImage -from utils.utils import svg_file +from utils.gtk_utils import svg_file # tunables _BARS = 8 # number of bars diff --git a/src/window/panel/notch/recording.py b/src/window/panel/notch/recording.py index 0b9332a..a9e93cf 100644 --- a/src/window/panel/notch/recording.py +++ b/src/window/panel/notch/recording.py @@ -5,7 +5,7 @@ from fabric.widgets.centerbox import CenterBox from fabric.widgets.label import Label from services.screencapture import screen_capture_service -from utils.utils import setup_cursor_hover, svg_file +from utils.gtk_utils import setup_cursor_hover, svg_file class RecordingIndicator(Box): diff --git a/src/window/screencapture/tool_button.py b/src/window/screencapture/tool_button.py index f9d8a37..380c0e1 100644 --- a/src/window/screencapture/tool_button.py +++ b/src/window/screencapture/tool_button.py @@ -1,6 +1,6 @@ from fabric.widgets.button import Button -from utils.utils import svg_file +from utils.gtk_utils import svg_file class ToolButton(Button): diff --git a/src/window/settings/__init__.py b/src/window/settings/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/window/settings/main.py b/src/window/settings/main.py index 660a66b..55a0cb5 100644 --- a/src/window/settings/main.py +++ b/src/window/settings/main.py @@ -9,7 +9,7 @@ from fabric.widgets.stack import Stack from services.config import config, on_config_change from shared.widgets.smooth_switch import SmoothSwitch -from utils.utils import setup_cursor_hover, svg_file +from utils.gtk_utils import setup_cursor_hover, svg_file class SettingsRow(CenterBox): diff --git a/src/window/spotlight/__init__.py b/src/window/spotlight/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/window/spotlight/core/loader.py b/src/window/spotlight/core/loader.py index 6b92dcc..48b3f60 100644 --- a/src/window/spotlight/core/loader.py +++ b/src/window/spotlight/core/loader.py @@ -1,7 +1,6 @@ import importlib import importlib.util import inspect -import subprocess import sys from dataclasses import dataclass from pathlib import Path @@ -237,53 +236,34 @@ class PluginLoader: def _create_venv(self, venv_dir: Path) -> bool: """Create a virtualenv using uv.""" - try: - result = subprocess.run( - ["uv", "venv", str(venv_dir)], - capture_output=True, - text=True, - timeout=30, - ) - if result.returncode != 0: - logger.error(f"[PluginLoader] uv venv failed: {result.stderr[:300]}") - return result.returncode == 0 - except FileNotFoundError: - logger.error("[PluginLoader] uv not found — install it first") - return False - except Exception as e: - logger.error(f"[PluginLoader] Failed to create venv: {e}") - return False + from utils.functions import run_command + + result = run_command(["uv", "venv", str(venv_dir)], timeout=30) + if result.returncode != 0: + logger.error(f"[PluginLoader] uv venv failed: {result.stderr[:300]}") + return result.returncode == 0 def _install_deps(self, venv_dir: Path, req_file: Path) -> bool: """Install dependencies from requirements.txt using uv.""" + from utils.functions import run_command + venv_python = venv_dir / "bin" / "python" - try: - result = subprocess.run( - [ - "uv", - "pip", - "install", - "-r", - str(req_file), - "--python", - str(venv_python), - ], - capture_output=True, - text=True, - timeout=120, - ) - if result.returncode != 0: - logger.error( - f"[PluginLoader] uv pip install failed: {result.stderr[:500]}" - ) - return False - return True - except FileNotFoundError: - logger.error("[PluginLoader] uv not found — install it first") - return False - except Exception as e: - logger.error(f"[PluginLoader] Failed to install deps: {e}") + result = run_command( + [ + "uv", + "pip", + "install", + "-r", + str(req_file), + "--python", + str(venv_python), + ], + timeout=120, + ) + if result.returncode != 0: + logger.error(f"[PluginLoader] uv pip install failed: {result.stderr[:500]}") return False + return True def _find_site_packages(self, venv_dir: Path) -> Path | None: """Find site-packages directory in a venv.""" diff --git a/src/window/spotlight/core/manager.py b/src/window/spotlight/core/manager.py index 98d9b89..c043b98 100644 --- a/src/window/spotlight/core/manager.py +++ b/src/window/spotlight/core/manager.py @@ -186,7 +186,7 @@ class PluginManager: entry.instance = None def _config_path(self) -> Path: - from utils.utils import toml_file + from utils.gtk_utils import toml_file return Path(toml_file("config.toml")).parent / "plugins.json" @@ -195,12 +195,12 @@ class PluginManager: if not path.exists(): self._disabled_ids = set() return - try: - with open(path) as f: - data = json.load(f) + from utils.functions import read_json_file + + data = read_json_file(str(path)) + if data is not None: self._disabled_ids = set(data.get("disabled", [])) - except Exception as e: - logger.error(f"[PluginManager] Failed to load disabled list: {e}") + else: self._disabled_ids = set() def _save_disabled_list(self) -> None: diff --git a/src/window/spotlight/plugins/application.py b/src/window/spotlight/plugins/application.py index 2d4fd03..e73cf93 100644 --- a/src/window/spotlight/plugins/application.py +++ b/src/window/spotlight/plugins/application.py @@ -75,7 +75,7 @@ class ApplicationPlugin(SpotlightPlugin): @staticmethod def _launch_app(app: DesktopApp) -> None: - from globalmenu.launch import launch_desktop_app + from window.globalmenu.launch import launch_desktop_app launch_desktop_app(app) diff --git a/src/window/spotlight/plugins/caffeine.py b/src/window/spotlight/plugins/caffeine.py index f3fd02a..d11986f 100644 --- a/src/window/spotlight/plugins/caffeine.py +++ b/src/window/spotlight/plugins/caffeine.py @@ -1,10 +1,10 @@ import os -import re import signal import subprocess from fabric.utils import logger +from utils.functions import format_duration, parse_timeout_string from window.spotlight.api import SearchResult, SpotlightPlugin _PID_FILE = os.path.join( @@ -16,24 +16,6 @@ _INHIBIT_WHY = "Modus Caffeine" # indefinite mode instead of relying on an in-process timer. _INDEFINITE_SECS = 100_000_000 -_DURATION_RE = re.compile( - r"^\s*(\d+)\s*(s|sec|secs|m|min|mins|h|hr|hrs|hour|hours)\s*$", - re.IGNORECASE, -) -_UNIT_SECS = { - "s": 1, - "sec": 1, - "secs": 1, - "m": 60, - "min": 60, - "mins": 60, - "h": 3600, - "hr": 3600, - "hrs": 3600, - "hour": 3600, - "hours": 3600, -} - class CaffeinePlugin(SpotlightPlugin): id = "caffeine" @@ -89,18 +71,21 @@ class CaffeinePlugin(SpotlightPlugin): @staticmethod def _parse_duration(text: str) -> int | None: - m = _DURATION_RE.match(text) - if not m: - return None - return int(m.group(1)) * _UNIT_SECS[m.group(2).lower()] + seconds = parse_timeout_string(text, in_seconds=True) + return seconds if seconds != 5 else None # 5 is the default on parse failure @staticmethod def _humanize(secs: int) -> str: - if secs >= 3600: - return f"{secs // 3600}h" - if secs >= 60: - return f"{secs // 60}m" - return f"{secs}s" + result = format_duration(secs) + # format_duration returns "N/A" for 0, but we want "0s" + if result == "N/A": + return "0s" + # format_duration returns "Xh Ym" or "Xm", we want shorter format + # For hours only: "1h" instead of "1h 0m" + if "h" in result and "m" in result: + hours = result.split("h")[0] + return f"{hours}h" + return result # --- inhibition ---------------------------------------------------- diff --git a/src/window/spotlight/plugins/emoji.py b/src/window/spotlight/plugins/emoji.py index f4074a9..4e8b999 100644 --- a/src/window/spotlight/plugins/emoji.py +++ b/src/window/spotlight/plugins/emoji.py @@ -164,7 +164,7 @@ class EmojiPlugin(SpotlightPlugin): result = self._normalize_annotations(raw) or None if result: - logger.info(f"[Emoji] Normalized {len(result)} emojis") + logger.debug(f"[Emoji] Normalized {len(result)} emojis") else: logger.error("[Emoji] Normalization returned empty") return result