chore: refactor imports and adopt new utils utilities
This commit is contained in:
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
@@ -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):
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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
@@ -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",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
Whitespace-only changes.
Whitespace-only changes.
@@ -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
|
||||
|
||||
File renamed without changes.
@@ -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");
|
||||
@@ -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
@@ -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.
@@ -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 = {}
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,4 +1,4 @@
|
||||
from utils.utils import toml_file
|
||||
from utils.gtk_utils import toml_file
|
||||
|
||||
PINNED_APPS_FILE = toml_file("dock.toml")
|
||||
|
||||
|
||||
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+")
|
||||
|
||||
File renamed without changes.
@@ -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
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Whitespace-only changes.
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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):
|
||||
|
||||
Whitespace-only changes.
@@ -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):
|
||||
|
||||
Whitespace-only changes.
@@ -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."""
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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 ----------------------------------------------------
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user