import datetime import os import shutil import subprocess from typing import Dict, List, Literal import gi import psutil from fabric.utils import exec_shell_command, exec_shell_command_async, get_relative_path from gi.repository import Gdk, GLib, Gtk from loguru import logger from .colors import Colors from .icons import distro_text_icons gi.require_version("Gtk", "3.0") class ExecutableNotFoundError(ImportError): """Raised when an executable is not found.""" def __init__(self, executable_name: str): super().__init__( f"{Colors.ERROR}Executable {Colors.UNDERLINE}{executable_name}{Colors.RESET} not found. Please install it using your package manager." # noqa: E501 ) # Function to escape the markup def parse_markup(text): return text # support for multiple monitors def for_monitors(widget): n = Gdk.Display.get_default().get_n_monitors() if Gdk.Display.get_default() else 1 return [widget(i) for i in range(n)] # Function to get the system icon theme def copy_theme(theme: str): destination_file = get_relative_path("../styles/theme.scss") source_file = get_relative_path(f"../styles/themes/{theme}.scss") if not os.path.exists(source_file): logger.warning( f"{Colors.WARNING}Warning: The theme file '{theme}.scss' was not found. Using default theme." # noqa: E501 ) source_file = get_relative_path("../styles/themes/catpuccin-mocha.scss") try: with open(source_file, "r") as source_file: content = source_file.read() # Open the destination file in write mode with open(destination_file, "w") as destination_file: destination_file.write(content) logger.info(f"{Colors.INFO}[THEME] '{theme}' applied successfully.") except FileNotFoundError: logger.error( f"{Colors.ERROR}Error: The theme file '{source_file}' was not found." ) exit(1) # Merge the parsed data with the default configuration def merge_defaults(data: dict, defaults: dict): return {**defaults, **data} # Validate the widgets def validate_widgets(parsed_data, default_config): layout = parsed_data["layout"] for section in layout: for widget in layout[section]: if widget not in default_config: raise ValueError( f"Invalid widget {widget} found in section {section}. Please check the widget name." # noqa: E501 ) # Function to exclude keys from a dictionary ) def exclude_keys(d: Dict, keys_to_exclude: List[str]) -> Dict: return {k: v for k, v in d.items() if k not in keys_to_exclude} # Function to format time in hours and minutes def format_time(secs: int): mm, _ = divmod(secs, 60) hh, mm = divmod(mm, 60) return "%d h %02d min" % (hh, mm) # Function to convert bytes to kilobytes, megabytes, or gigabytes def convert_bytes(bytes: int, to: Literal["kb", "mb", "gb"], format_spec=".1f"): multiplier = 1 if to == "mb": multiplier = 2 elif to == "gb": multiplier = 3 return f"{format(bytes / (1024**multiplier), format_spec)}{to.upper()}" # Function to get the system uptime def uptime(): boot_time = psutil.boot_time() now = datetime.datetime.now() diff = now.timestamp() - boot_time # Convert the difference in seconds to hours and minutes hours, remainder = divmod(diff, 3600) minutes, _ = divmod(remainder, 60) return f"{int(hours):02}:{int(minutes):02}" # Function to convert seconds to milliseconds def convert_seconds_to_milliseconds(seconds: int): return seconds * 1000 # Function to check if an icon exists, otherwise use a fallback icon def check_icon_exists(icon_name: str, fallback_icon: str) -> str: if Gtk.IconTheme.get_default().has_icon(icon_name): return icon_name return fallback_icon # Function to execute a shell command asynchronously def play_sound(file: str): exec_shell_command_async(f"play {file}", None) # Function to get the distro icon def get_distro_icon(): distro_id = GLib.get_os_info("ID") # Search for the icon in the list return distro_text_icons.get(distro_id, "") # Function to check if an executable exists def executable_exists(executable_name): executable_path = shutil.which(executable_name) return bool(executable_path) def send_notification( title: str, body: str, urgency: Literal["low", "normal", "critical"], icon=None, app_name="Application", timeout=None, ): """ Sends a notification using the notify-send command. :param title: The title of the notification :param body: The message body of the notification :param urgency: The urgency of the notification ('low', 'normal', 'critical') :param icon: Optional icon for the notification :param app_name: The application name that is sending the notification :param timeout: Optional timeout in milliseconds (e.g., 5000 for 5 seconds) """ # Base command command = [ "notify-send", "--urgency", urgency, "--app-name", app_name, title, body, ] # Add icon if provided if icon: command.extend(["--icon", icon]) if timeout is not None: command.extend(["-t", str(timeout)]) try: subprocess.run(command, check=True) except subprocess.CalledProcessError as e: print(f"Failed to send notification: {e}") # Function to get the relative time def get_relative_time(mins: int) -> str: # Seconds if mins == 0: return "now" # Minutes if mins < 60: return f"{mins} minute{'s' if mins > 1 else ''} ago" # Hours if mins < 1440: hours = mins // 60 return f"{hours} hour{'s' if hours > 1 else ''} ago" # Days days = mins // 1440 return f"{days} day{'s' if days > 1 else ''} ago" # Function to get the percentage of a value def convert_to_percent( current: int | float, max: int | float, is_int=True ) -> int | float: if is_int: return int((current / max) * 100) else: return (current / max) * 100 # Function to ensure the directory exists def ensure_dir_exists(path: str): if not os.path.exists(path): os.makedirs(path) # Function to unique list def unique_list(lst) -> List: return list(set(lst)) # Function to check if an app is running def is_app_running(app_name: str) -> bool: return len(exec_shell_command(f"pidof {app_name}")) != 0