diff --git a/scripts/gdm.py b/scripts/gdm.py index 5a573a4..a5c23cf 100644 --- a/scripts/gdm.py +++ b/scripts/gdm.py @@ -6,9 +6,10 @@ from .theme import Theme from .utils import remove_properties, remove_keywords from . import config from .utils.alternatives_updater import AlternativesUpdater -from .utils.console import Console, Color, Format +from scripts.utils.logger.console import Console, Color, Format from .utils.files_labeler import FilesLabeler -from .utils.gresource import Gresource, GresourceBackupNotFoundError +from .utils.gresource import GresourceBackupNotFoundError +from .utils.gresource.gresource import Gresource class GlobalTheme: @@ -40,7 +41,7 @@ class GlobalTheme: self.__gresource_file = os.path.join(self.destination_folder, self.destination_file) self.__gresource_temp_folder = os.path.join(self.temp_folder, config.extracted_gdm_folder) - self.__gresource = Gresource(self.destination_file, self.__gresource_temp_folder, self.destination_folder) + self.__gresource = Gresource(self.destination_file, self.__gresource_temp_folder, self.destination_folder, logger_factory=Console()) def prepare(self): if self.__is_installed(): diff --git a/scripts/install/global_theme_installer.py b/scripts/install/global_theme_installer.py index a45a341..5370a73 100644 --- a/scripts/install/global_theme_installer.py +++ b/scripts/install/global_theme_installer.py @@ -3,7 +3,7 @@ import os from scripts import config from scripts.gdm import GlobalTheme from scripts.install.theme_installer import ThemeInstaller -from scripts.utils.console import Console, Color, Format +from scripts.utils.logger.console import Console, Color, Format class GlobalThemeInstaller(ThemeInstaller): diff --git a/scripts/install/local_theme_installer.py b/scripts/install/local_theme_installer.py index 7648003..266f356 100644 --- a/scripts/install/local_theme_installer.py +++ b/scripts/install/local_theme_installer.py @@ -4,7 +4,7 @@ from scripts import config from scripts.install.theme_installer import ThemeInstaller from scripts.theme import Theme from scripts.utils import remove_files -from scripts.utils.console import Console, Color, Format +from scripts.utils.logger.console import Console, Color, Format class LocalThemeInstaller(ThemeInstaller): diff --git a/scripts/theme.py b/scripts/theme.py index 05573de..7c3241f 100644 --- a/scripts/theme.py +++ b/scripts/theme.py @@ -8,7 +8,7 @@ from .utils import ( copy_files, # copy files from source to destination destination_return, # copied/modified theme location generate_file) # combine files from folder to one file -from .utils.console import Console, Color, Format +from scripts.utils.logger.console import Console, Color, Format class Theme: diff --git a/scripts/utils/alternatives_updater.py b/scripts/utils/alternatives_updater.py index f5e4ff8..1f9928a 100644 --- a/scripts/utils/alternatives_updater.py +++ b/scripts/utils/alternatives_updater.py @@ -2,7 +2,7 @@ import functools import subprocess from typing import TypeAlias -from scripts.utils.console import Console +from scripts.utils.logger.console import Console PathString: TypeAlias = str | bytes diff --git a/scripts/utils/gnome.py b/scripts/utils/gnome.py index 9448766..83e65cc 100644 --- a/scripts/utils/gnome.py +++ b/scripts/utils/gnome.py @@ -2,7 +2,7 @@ import subprocess import time from scripts import config -from scripts.utils.console import Console, Format, Color +from scripts.utils.logger.console import Console, Format, Color from scripts.utils.parse_folder import parse_folder diff --git a/scripts/utils/gresource.py b/scripts/utils/gresource.py deleted file mode 100644 index 9091c19..0000000 --- a/scripts/utils/gresource.py +++ /dev/null @@ -1,171 +0,0 @@ -import os -import subprocess -import textwrap -from pathlib import Path - -from scripts.utils.console import Console - - -class GresourceBackupNotFoundError(FileNotFoundError): - def __init__(self, location: str = None): - if location: - super().__init__(f"Gresource backup file not found: {location}") - else: - super().__init__("Gresource backup file not found.") - -class MissingDependencyError(Exception): - def __init__(self, dependency: str): - super().__init__(f"Missing required dependency: {dependency}") - self.dependency = dependency - - -class Gresource: - """Handles the extraction and compilation of gresource files for GNOME Shell themes.""" - - def __init__(self, gresource_file: str, temp_folder: str, destination: str): - """ - :param gresource_file: The name of the gresource file to be processed. - :param temp_folder: The temporary folder where resources will be extracted. - :param destination: The destination folder where the compiled gresource file will be saved. - """ - self.gresource_file = gresource_file - self.temp_folder = temp_folder - self.destination = destination - - self._temp_gresource = os.path.join(temp_folder, gresource_file) - self._destination_gresource = os.path.join(destination, gresource_file) - self._active_source_gresource = self._destination_gresource - self._backup_gresource = os.path.join(destination, f"{gresource_file}.backup") - self._gresource_xml = os.path.join(temp_folder, f"{gresource_file}.xml") - - def use_backup_gresource(self): - if not os.path.exists(self._backup_gresource): - raise GresourceBackupNotFoundError(self._backup_gresource) - self._active_source_gresource = self._backup_gresource - - def extract(self): - extract_line = Console.Line() - extract_line.update("Extracting gresource files...") - - resources = self._get_resources_list() - self._extract_resources(resources) - - extract_line.success("Extracted gresource files.") - - def _get_resources_list(self): - resources_list_response = subprocess.run( - ["gresource", "list", self._active_source_gresource], - capture_output=True, text=True, check=False - ) - - if resources_list_response.stderr: - raise Exception(f"gresource could not process the theme file: {self._active_source_gresource}") - - return resources_list_response.stdout.strip().split("\n") - - def _extract_resources(self, resources: list[str]): - prefix = "/org/gnome/shell/theme/" - try: - for resource in resources: - resource_path = resource.replace(prefix, "") - output_path = os.path.join(self.temp_folder, resource_path) - os.makedirs(os.path.dirname(output_path), exist_ok=True) - - with open(output_path, 'wb') as f: - subprocess.run( - ["gresource", "extract", self._active_source_gresource, resource], - stdout=f, check=True - ) - except FileNotFoundError as e: - if "gresource" in str(e): - self._raise_gresource_error(e) - raise - - @staticmethod - def _raise_gresource_error(e: Exception): - print("Error: 'gresource' command not found.") - print("Please install the glib2-devel package:") - print(" - For Fedora/RHEL: sudo dnf install glib2-devel") - print(" - For Ubuntu/Debian: sudo apt install libglib2.0-dev") - print(" - For Arch: sudo pacman -S glib2-devel") - raise MissingDependencyError("glib2-devel") from e - - def compile(self): - compile_line = Console.Line() - compile_line.update("Compiling gnome-shell theme...") - - self._create_gresource_xml() - self._compile_resources() - - compile_line.success("Theme compiled.") - - def _create_gresource_xml(self): - with open(self._gresource_xml, 'w') as gresource_xml: - gresource_xml.write(self._generate_gresource_xml()) - - def _generate_gresource_xml(self): - files_to_include = self._get_files_to_include() - nl = "\n" # fstring doesn't support newline character - return textwrap.dedent(f""" - - - - {nl.join(files_to_include)} - - - """) - - def _get_files_to_include(self): - temp_path = Path(self.temp_folder) - return [ - f"{file.relative_to(temp_path)}" - for file in temp_path.glob('**/*') - if file.is_file() - ] - - def _compile_resources(self): - try: - subprocess.run(["glib-compile-resources", - "--sourcedir", self.temp_folder, - "--target", self._temp_gresource, - self._gresource_xml - ], - cwd=self.temp_folder, check=True) - except FileNotFoundError as e: - if "glib-compile-resources" in str(e): - self._raise_gresource_error(e) - raise - - def backup(self): - backup_line = Console.Line() - backup_line.update("Backing up gresource files...") - - subprocess.run(["cp", "-aT", - self._destination_gresource, - self._backup_gresource], - check=True) - - backup_line.success("Backed up gresource files.") - - def restore(self): - if not os.path.exists(self._backup_gresource): - raise GresourceBackupNotFoundError(self._backup_gresource) - - subprocess.run(["mv", "-f", - self._backup_gresource, - self._destination_gresource], - check=True) - - - def move(self): - move_line = Console.Line() - move_line.update("Moving gresource files...") - - subprocess.run(["cp", "-f", - self._temp_gresource, - self._destination_gresource], - check=True) - - subprocess.run(["chmod", "644", self._destination_gresource], check=True) - - move_line.success("Moved gresource files.") \ No newline at end of file diff --git a/scripts/utils/gresource/__init__.py b/scripts/utils/gresource/__init__.py new file mode 100644 index 0000000..b69e6ab --- /dev/null +++ b/scripts/utils/gresource/__init__.py @@ -0,0 +1,21 @@ +class GresourceBackupNotFoundError(FileNotFoundError): + def __init__(self, location: str = None): + if location: + super().__init__(f"Gresource backup file not found: {location}") + else: + super().__init__("Gresource backup file not found.") + + +class MissingDependencyError(Exception): + def __init__(self, dependency: str): + super().__init__(f"Missing required dependency: {dependency}") + self.dependency = dependency + + +def raise_gresource_error(tool: str, e: Exception): + print(f"Error: '{tool}' command not found.") + print("Please install the glib2-devel package:") + print(" - For Fedora/RHEL: sudo dnf install glib2-devel") + print(" - For Ubuntu/Debian: sudo apt install libglib2.0-dev") + print(" - For Arch: sudo pacman -S glib2-devel") + raise MissingDependencyError("glib2-devel") from e diff --git a/scripts/utils/gresource/gresource.py b/scripts/utils/gresource/gresource.py new file mode 100644 index 0000000..31295bb --- /dev/null +++ b/scripts/utils/gresource/gresource.py @@ -0,0 +1,46 @@ +import os + +from scripts.utils.gresource.gresource_backuper import GresourceBackuperManager +from scripts.utils.gresource.gresource_complier import GresourceCompiler +from scripts.utils.gresource.gresource_extractor import GresourceExtractor +from scripts.utils.gresource.gresource_mover import GresourceMover +from scripts.utils.logger.logger import LoggerFactory + + +class Gresource: + """Orchestrator for gresource files. Manages the extraction, compilation, and backup of gresource files.""" + + def __init__(self, gresource_file: str, temp_folder: str, destination: str, logger_factory: LoggerFactory): + """ + :param gresource_file: The name of the gresource file to be processed. + :param temp_folder: The temporary folder where resources will be extracted. + :param destination: The destination folder where the compiled gresource file will be saved. + """ + self.gresource_file = gresource_file + self.temp_folder = temp_folder + self.destination = destination + self.logger_factory = logger_factory + + self._temp_gresource = os.path.join(temp_folder, gresource_file) + self._destination_gresource = os.path.join(destination, gresource_file) + self._active_source_gresource = self._destination_gresource + + self._backuper = GresourceBackuperManager(self._destination_gresource, logger_factory=self.logger_factory) + + def use_backup_gresource(self): + self._active_source_gresource = self._backuper.get_backup() + + def extract(self): + GresourceExtractor(self._active_source_gresource, self.temp_folder, logger_factory=self.logger_factory).extract() + + def compile(self): + GresourceCompiler(self.temp_folder, self._temp_gresource, logger_factory=self.logger_factory).compile() + + def backup(self): + self._backuper.backup() + + def restore(self): + self._backuper.restore() + + def move(self): + GresourceMover(self._temp_gresource, self._destination_gresource, logger_factory=self.logger_factory).move() diff --git a/scripts/utils/gresource/gresource_backuper.py b/scripts/utils/gresource/gresource_backuper.py new file mode 100644 index 0000000..45beb0b --- /dev/null +++ b/scripts/utils/gresource/gresource_backuper.py @@ -0,0 +1,52 @@ +import os +import shutil +import subprocess + +from scripts.utils.gresource import GresourceBackupNotFoundError +from scripts.utils.logger.logger import LoggerFactory + + +class GresourceBackuperManager: + def __init__(self, destination_file: str, logger_factory: LoggerFactory): + self.destination_file = destination_file + self._backup_file = f"{destination_file}.backup" + self._backuper = GresourceBackuper(destination_file, self._backup_file, logger_factory) + + def backup(self): + self._backuper.backup() + + def restore(self): + self._backuper.restore() + + def get_backup(self) -> str: + return self._backuper.get_backup() + + +class GresourceBackuper: + def __init__(self, destination_file: str, backup_file, logger_factory: LoggerFactory): + self.destination_file = destination_file + self.backup_file = backup_file + self.logger_factory = logger_factory + + def get_backup(self) -> str: + if not os.path.exists(self.backup_file): + raise GresourceBackupNotFoundError(self.backup_file) + return self.backup_file + + def backup(self): + backup_line = self.logger_factory.create_logger() + backup_line.update("Backing up gresource files...") + + if os.path.exists(self.backup_file): + os.remove(self.backup_file) + + shutil.copy2(self.destination_file, self.backup_file) + # subprocess.run(["cp", "-aT", self.destination_file, self.backup_file], check=True) + + backup_line.success("Backed up gresource files.") + + def restore(self): + if not os.path.exists(self.backup_file): + raise GresourceBackupNotFoundError(self.backup_file) + + subprocess.run(["mv", "-f", self.backup_file, self.destination_file], check=True) diff --git a/scripts/utils/gresource/gresource_complier.py b/scripts/utils/gresource/gresource_complier.py new file mode 100644 index 0000000..2d7698b --- /dev/null +++ b/scripts/utils/gresource/gresource_complier.py @@ -0,0 +1,63 @@ +import subprocess +import textwrap +from pathlib import Path + +from scripts.utils.gresource import raise_gresource_error +from scripts.utils.logger.logger import LoggerFactory + + +class GresourceCompiler: + def __init__(self, source_folder: str, target_file: str, logger_factory: LoggerFactory): + self.source_folder = source_folder + self.target_file = target_file + self.gresource_xml = target_file + ".xml" + self.logger_factory = logger_factory + + def compile(self): + compile_line = self.logger_factory.create_logger() + compile_line.update("Compiling gnome-shell theme...") + + self._create_gresource_xml() + self._compile_resources() + + compile_line.success("Compiled gnome-shell theme.") + + def _create_gresource_xml(self): + with open(self.gresource_xml, 'w') as gresource_xml: + gresource_xml.write(self._generate_gresource_xml()) + + def _generate_gresource_xml(self): + files_to_include = self._get_files_to_include() + nl = "\n" # fstring doesn't support newline character + return textwrap.dedent(f""" + + + + {nl.join(files_to_include)} + + + """) + + def _get_files_to_include(self): + source_path = Path(self.source_folder) + return [ + f"{file.relative_to(source_path)}" + for file in source_path.glob('**/*') + if file.is_file() + ] + + def _compile_resources(self): + try: + self._try_compile_resources() + except FileNotFoundError as e: + if "glib-compile-resources" in str(e): + raise_gresource_error("glib-compile-resources", e) + raise + + def _try_compile_resources(self): + subprocess.run(["glib-compile-resources", + "--sourcedir", self.source_folder, + "--target", self.target_file, + self.gresource_xml + ], + cwd=self.source_folder, check=True) diff --git a/scripts/utils/gresource/gresource_extractor.py b/scripts/utils/gresource/gresource_extractor.py new file mode 100644 index 0000000..a84e3f8 --- /dev/null +++ b/scripts/utils/gresource/gresource_extractor.py @@ -0,0 +1,53 @@ +import os +import subprocess + +from scripts.utils.gresource import raise_gresource_error +from scripts.utils.logger.logger import LoggerFactory + + +class GresourceExtractor: + def __init__(self, gresource_path: str, extract_folder: str, logger_factory: LoggerFactory): + self.gresource_path = gresource_path + self.extract_folder = extract_folder + self.logger_factory = logger_factory + + def extract(self): + extract_line = self.logger_factory.create_logger() + extract_line.update("Extracting gresource files...") + + resources = self._get_resources_list() + self._extract_resources(resources) + + extract_line.success("Extracted gresource files.") + + def _get_resources_list(self): + resources_list_response = subprocess.run( + ["gresource", "list", self.gresource_path], + capture_output=True, text=True, check=False + ) + + if resources_list_response.stderr: + raise Exception(f"gresource could not process the theme file: {self.gresource_path}") + + return resources_list_response.stdout.strip().split("\n") + + def _extract_resources(self, resources: list[str]): + try: + self._try_extract_resources(resources) + except FileNotFoundError as e: + if "gresource" in str(e): + raise_gresource_error("gresource", e) + raise + + def _try_extract_resources(self, resources: list[str]): + prefix = "/org/gnome/shell/theme/" + for resource in resources: + resource_path = resource.replace(prefix, "") + output_path = os.path.join(self.extract_folder, resource_path) + os.makedirs(os.path.dirname(output_path), exist_ok=True) + + with open(output_path, 'wb') as f: + subprocess.run( + ["gresource", "extract", self.gresource_path, resource], + stdout=f, check=True + ) diff --git a/scripts/utils/gresource/gresource_mover.py b/scripts/utils/gresource/gresource_mover.py new file mode 100644 index 0000000..4c8a34a --- /dev/null +++ b/scripts/utils/gresource/gresource_mover.py @@ -0,0 +1,23 @@ +import subprocess + +from scripts.utils.logger.logger import LoggerFactory + + +class GresourceMover: + def __init__(self, source_file: str, destination_file: str, logger_factory: LoggerFactory): + self.source_file = source_file + self.destination_file = destination_file + self.logger_factory = logger_factory + + def move(self): + move_line = self.logger_factory.create_logger() + move_line.update("Moving gresource files...") + + subprocess.run(["cp", "-f", + self.source_file, + self.destination_file], + check=True) + + subprocess.run(["chmod", "644", self.destination_file], check=True) + + move_line.success("Moved gresource files.") diff --git a/scripts/utils/logger/__init__.py b/scripts/utils/logger/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/scripts/utils/console.py b/scripts/utils/logger/console.py similarity index 88% rename from scripts/utils/console.py rename to scripts/utils/logger/console.py index 53d2738..b04552f 100644 --- a/scripts/utils/console.py +++ b/scripts/utils/logger/console.py @@ -3,14 +3,24 @@ import threading from enum import Enum from typing import Optional +from scripts.utils.logger.logger import LoggerFactory, Logger -class Console: + +class Console(LoggerFactory): """Manages console output for concurrent processes with line tracking""" _print_lock = threading.Lock() _line_mapping = {} _next_line = 0 - class Line: + def create_logger(self, name: Optional[str]=None) -> 'Console.Line': + """ + Create a logger instance with the given name. + :param name: Name of the logger. + :return: Logger instance. + """ + return Console.Line(name) + + class Line(Logger): def __init__(self, name: Optional[str]=None): """Initialize a new managed line""" self.name = name or f"line_{Console._next_line}" diff --git a/scripts/utils/logger/logger.py b/scripts/utils/logger/logger.py new file mode 100644 index 0000000..e3a00e3 --- /dev/null +++ b/scripts/utils/logger/logger.py @@ -0,0 +1,36 @@ +from abc import ABC, abstractmethod +from typing import Optional + + +class LoggerFactory(ABC): + @staticmethod + @abstractmethod + def create_logger(name: Optional[str] = None) -> 'Logger': + """ + Create a logger instance with the given name. + :param name: Name of the logger. + :return: Logger instance. + """ + pass + + +class Logger(ABC): + @abstractmethod + def update(self, message: str): + pass + + @abstractmethod + def success(self, message): + pass + + @abstractmethod + def error(self, message): + pass + + @abstractmethod + def warn(self, message): + pass + + @abstractmethod + def info(self, message): + pass \ No newline at end of file diff --git a/scripts/utils/remove_files.py b/scripts/utils/remove_files.py index ffb9173..7a1d9af 100644 --- a/scripts/utils/remove_files.py +++ b/scripts/utils/remove_files.py @@ -7,7 +7,7 @@ import shutil from collections import defaultdict from typing import Any -from .console import Console, Color, Format +from scripts.utils.logger.console import Console, Color, Format from .parse_folder import parse_folder from .. import config import os