chore: refactor imports and adopt new utils utilities

This commit is contained in:
sankalp committed 2026-07-14 18:45:52 +05:45
1 parent b2114f52b1
commit 6f2d0bf444
65 files changed
+221 -266

No files matched your search

File renamed without changes.

Before

Width:  |  Height:  |  Size: 604 B

After

Width:  |  Height:  |  Size: 604 B

File renamed without changes.

Before

Width:  |  Height:  |  Size: 821 B

After

Width:  |  Height:  |  Size: 821 B

+2 -2
View File
@@ -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):
+1 -1
View File
@@ -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"))
+1 -1
View File
@@ -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:
+3 -6
View File
@@ -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:
+4 -2
View File
@@ -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:
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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"))
+3 -2
View File
@@ -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:
-4
View File
@@ -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: ...
+1 -1
View File
@@ -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"
File renamed without changes.
+35 -35
View File
@@ -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",
),
]
View File
Whitespace-only changes.
View File
Whitespace-only changes.
+3 -6
View File
@@ -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
+1 -1
View File
@@ -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");
+2 -2
View File
@@ -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.
"""
+43 -25
View File
@@ -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
File renamed without changes.
+3 -6
View File
@@ -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 = {}
+1 -1
View File
@@ -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(
+6 -6
View File
@@ -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)
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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):
+3 -6
View File
@@ -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()
+4 -4
View File
@@ -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)
+1 -1
View File
@@ -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):
+4 -4
View File
@@ -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:
+4 -6
View File
@@ -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:
+1 -1
View File
@@ -1,4 +1,4 @@
from utils.utils import toml_file
from utils.gtk_utils import toml_file
PINNED_APPS_FILE = toml_file("dock.toml")
View File
Whitespace-only changes.
File renamed without changes.
@@ -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:
File renamed without changes.
@@ -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:
@@ -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+")
@@ -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'<node name="([^"]+)"')
+1 -1
View File
@@ -14,7 +14,7 @@ from gi.repository import GtkSessionLock # pyright: ignore[reportAttributeAcces
from shared.widgets.circle_image import CircleImage as Image
from utils.functions import set_process_name
from utils.utils import svg_file
from utils.gtk_utils import svg_file
from window.panel.components.indicators import (
BatteryIndicator,
BluetoothIndicator,
+5 -11
View File
@@ -15,7 +15,7 @@ from services.modus import notification_service
from shared.widgets.clipping_box import ClippingBox
from shared.widgets.custom_image import CustomImage
from shared.window.applet_window import AppletWindow
from utils.functions import escape_markup_text
from utils.functions import clear_children, escape_markup_text
from window.notification.notification import (
NotificationWidget,
cache_notification_icon,
@@ -704,8 +704,7 @@ class NotificationCenter(AppletWindow):
self.group_widgets.clear()
# Clear notifications box
for child in self.notifications_box.get_children():
child.destroy()
clear_children(self.notifications_box)
# Group notifications by app name and preload assets with debugging
for cached_notification in notification_service.cached_notifications:
@@ -811,10 +810,7 @@ class NotificationCenter(AppletWindow):
def _refresh_group_widget(self, group_widget):
"""Refresh a group widget's content"""
try:
# Remove and destroy existing children
for child in group_widget.get_children():
group_widget.remove(child)
child.destroy()
clear_children(group_widget)
# Recreate content (collapsed + expanded states)
group_widget.create_collapsed_state()
@@ -870,8 +866,7 @@ class NotificationCenter(AppletWindow):
# Clear all remaining cached notification images AND icons
cleanup_all_notification_caches()
for child in self.notifications_box.get_children():
child.destroy()
clear_children(self.notifications_box)
except Exception as e:
logger.error(f"Error clearing notification groups: {e}")
@@ -892,8 +887,7 @@ class NotificationCenter(AppletWindow):
# Clear all remaining cached notification images AND icons when clear all is clicked
cleanup_all_notification_caches() # Clear ALL caches (icons + images)
for child in self.notifications_box.get_children():
child.destroy()
clear_children(self.notifications_box)
notification_service.clear_all_cached_notifications()
self.hide()
+1 -1
View File
@@ -4,7 +4,7 @@ from fabric.audio import Audio
from fabric.utils import logger
from fabric.widgets.scale import ScaleMark
from utils.utils import svg_file
from utils.gtk_utils import svg_file
from .animated_scale import AnimatedScale
from .base import BaseOSDContainer
+1 -1
View File
@@ -4,7 +4,7 @@ from fabric.utils import logger
from fabric.widgets.scale import ScaleMark
from services.brightness import Brightness
from utils.utils import svg_file
from utils.gtk_utils import svg_file
from .animated_scale import AnimatedScale
from .base import BaseOSDContainer
View File
Whitespace-only changes.
+3 -5
View File
@@ -7,8 +7,8 @@ from fabric.widgets.button import Button
from fabric.widgets.label import Label
from shared.window.applet_window import AppletWindow
from utils.functions import run_command, thread
from utils.utils import setup_cursor_hover, svg_file, toml_file
from utils.functions import clear_children, run_command, thread
from utils.gtk_utils import setup_cursor_hover, svg_file, toml_file
MODS_CONFIG_PATH = Path(toml_file("mods.toml"))
RELOAD_DELAY_MS = 200
@@ -167,9 +167,7 @@ class CustomMods(Box):
return True
def _rebuild(self, mods):
for child in list(self.get_children()):
self.remove(child)
child.destroy()
clear_children(self)
for popup in self._mod_popups.values():
if popup:
+2 -2
View File
@@ -5,12 +5,12 @@ from fabric.widgets.button import Button
from fabric.widgets.centerbox import CenterBox
from fabric.widgets.label import Label
from globalmenu.service import get_global_menu_service
from shared.dialogs.about import AboutApp, get_about_window
from shared.window.dropdown import ModusDropdown, dropdown_divider, dropdowns
from utils.app_name_resolver import format_window
from utils.gtk_utils import setup_cursor_hover
from utils.roam import modus_service
from utils.utils import setup_cursor_hover
from window.globalmenu.service import get_global_menu_service
from window.settings.main import get_settings_window
+5 -8
View File
@@ -9,8 +9,8 @@ from services.network import NetworkClient
from shared.window.applet_window import AppletWindow
from shared.window.battery_widget import BatteryControl
from utils.functions import format_duration, get_wifi_icon_for_strength
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.bluetooth import BluetoothConnections
from window.controlcenter.wifi import WifiConnections
@@ -387,9 +387,6 @@ class BatteryIndicator(Box):
def _get_time_to_full(self):
return self.battery_service.time_to_full
def _format_time(self, seconds: int) -> 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":
+9 -7
View File
@@ -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
+4 -4
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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):
+1 -1
View File
@@ -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):
View File
Whitespace-only changes.
+1 -1
View File
@@ -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):
View File
Whitespace-only changes.
+23 -43
View File
@@ -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."""
+6 -6
View File
@@ -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:
+1 -1
View File
@@ -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)
+13 -28
View File
@@ -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 ----------------------------------------------------
+1 -1
View File
@@ -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