diff --git a/scripts/gdm.py b/scripts/gdm.py deleted file mode 100644 index 9670f96..0000000 --- a/scripts/gdm.py +++ /dev/null @@ -1,159 +0,0 @@ -import os -from typing import Optional - -from .install.colors_definer import ColorsDefiner -from scripts.utils.theme.theme import Theme -from .types.theme_base import ThemeBase -from .utils import remove_properties, remove_keywords -from . import config -from .utils.alternatives_updater import AlternativesUpdater -from scripts.utils.logger.console import Console, Color, Format -from .utils.command_runner.subprocess_command_runner import SubprocessCommandRunner -from .utils.files_labeler import FilesLabeler -from .utils.gresource import GresourceBackupNotFoundError -from .utils.gresource.gresource import Gresource -from .utils.theme.gnome_shell_theme_builder import GnomeShellThemeBuilder - - -class GlobalTheme(ThemeBase): - """Class to install global theme for GDM""" - def __init__(self, - colors_json: ColorsDefiner, source_folder: str, - destination_folder: str, destination_file: str, temp_folder: str | bytes, - mode: Optional[str] = None, is_filled = False - ): - """ - :param colors_json: location of a JSON file with color values - :param source_folder: raw theme location - :param destination_folder: folder where themes will be installed - :param temp_folder: folder where files will be collected - :param mode: theme mode (light or dark) - :param is_filled: if True, the theme will be filled - """ - self.colors_json = colors_json - self.source_folder = source_folder - self.destination_folder = destination_folder - self.destination_file = destination_file - self.temp_folder = temp_folder - self.is_filled = is_filled - self.mode = mode - - self.themes: list[ThemePrepare] = [] - - self.__is_installed_trigger = "\n/* Marble theme */\n" - 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, - logger_factory=Console(), runner=SubprocessCommandRunner()) - - def prepare(self): - if self.__is_installed(): - Console.Line().info("Theme is installed. Reinstalling...") - self.__gresource.use_backup_gresource() - - self.__gresource.extract() - self.__find_themes() - - def __is_installed(self) -> bool: - if not hasattr(self, '__is_installed_cached'): - with open(self.__gresource_file, "rb") as f: - self.__is_installed_cached = self.__is_installed_trigger.encode() in f.read() - - return self.__is_installed_cached - - def __find_themes(self): - extracted_theme_files = os.listdir(self.__gresource_temp_folder) - - allowed_modes = ("dark", "light") - allowed_css = ("gnome-shell-dark", "gnome-shell-light", "gnome-shell") - - for style_name in allowed_css: - style_file = style_name + ".css" - if style_file in extracted_theme_files: - last_mode = style_name.split("-")[-1] - mode = last_mode if last_mode in allowed_modes else None - self.__append_theme(style_name, mode=mode or self.mode, label=mode) - - def __append_theme(self, theme_type: str, mode = None, label: Optional[str] = None): - """Helper to create theme objects""" - theme_builder = GnomeShellThemeBuilder(self.colors_json) - theme_builder.with_temp_folder(self.temp_folder) - theme_builder.with_theme_name(theme_type) - theme_builder.destination_folder = self.__gresource_temp_folder - theme_builder.with_mode(mode) - theme_builder.filled(self.is_filled) - - theme = theme_builder.build() - theme.prepare() - theme_file = os.path.join(self.__gresource_temp_folder, f"{theme_type}.css") - self.themes.append(ThemePrepare(theme=theme, theme_file=theme_file, label=label)) - - def install(self, hue, sat=None): - """Install theme globally""" - if os.geteuid() != 0: - raise Exception("Root privileges required to install GDM theme") - - self.__generate_themes(hue, 'Marble', sat) - - self.__gresource.compile() - if not self.__is_installed(): - self.__gresource.backup() - self.__gresource.move() - - self.__update_alternatives() - - def __generate_themes(self, hue: int, color: str, sat: Optional[int] = None): - """Generate theme files for gnome-shell-theme.gresource.xml""" - for theme_prepare in self.themes: - if theme_prepare.label is not None: - temp_folder = theme_prepare.theme.temp_folder - main_styles = theme_prepare.theme.main_styles - FilesLabeler(temp_folder, main_styles).append_label(theme_prepare.label) - - remove_keywords(theme_prepare.theme_file, "!important") - remove_properties(theme_prepare.theme_file, "background-color", "color", "box-shadow", "border-radius") - - self.__add_gnome_styles(theme_prepare.theme) - - theme_prepare.theme.install(hue, color, sat, destination=self.__gresource_temp_folder) - - def __add_gnome_styles(self, theme: Theme): - """Add gnome styles to the start of the file""" - with open(f"{theme.destination_folder}/{theme.theme_name}.css", 'r') as gnome_theme: - gnome_styles = gnome_theme.read() + self.__is_installed_trigger - theme.add_to_start(gnome_styles) - - def __update_alternatives(self): - link = os.path.join(self.destination_folder, config.ubuntu_gresource_link) - name = config.ubuntu_gresource_link - path = os.path.join(self.destination_folder, self.destination_file) - AlternativesUpdater.install_and_set(link, name, path) - - def remove(self): - if self.__is_installed(): - removing_line = Console.Line() - removing_line.update("Theme is installed. Removing...") - - try: - self.__gresource.restore() - removing_line.success("Global theme removed successfully. Restart GDM to apply changes.") - except GresourceBackupNotFoundError: - formatted_shell = Console.format("gnome-shell", color=Color.BLUE, format_type=Format.BOLD) - removing_line.error(f"Backup file not found. Try reinstalling {formatted_shell} package.") - else: - Console.Line().error("Theme is not installed. Nothing to remove.") - Console.Line().warn("If theme is still installed globally, try reinstalling gnome-shell package.") - - def __remove_alternatives(self): - name = config.ubuntu_gresource_link - path = os.path.join(self.destination_folder, self.destination_file) - AlternativesUpdater.remove(name, path) - - -class ThemePrepare: - """Theme data class prepared for installation""" - def __init__(self, theme: Theme, theme_file, label: Optional[str] = None): - self.theme = theme - self.theme_file = theme_file - self.label = label \ No newline at end of file diff --git a/scripts/install/global_theme_installer.py b/scripts/install/global_theme_installer.py index e6d92c7..0550081 100644 --- a/scripts/install/global_theme_installer.py +++ b/scripts/install/global_theme_installer.py @@ -1,14 +1,11 @@ -import os - -from scripts import config -from scripts.gdm import GlobalTheme from scripts.install.theme_installer import ThemeInstaller +from scripts.utils.global_theme.gdm import GDMTheme +from scripts.utils.global_theme.gdm_builder import GdmBuilder from scripts.utils.logger.console import Console, Color, Format -from theme import SourceFolder class GlobalThemeInstaller(ThemeInstaller): - theme: GlobalTheme + theme: GDMTheme def remove(self): gdm_rm_status = self.theme.remove() @@ -16,14 +13,10 @@ class GlobalThemeInstaller(ThemeInstaller): print("GDM theme removed successfully.") def _define_theme(self): - gdm_temp = os.path.join(config.temp_folder, config.gdm_folder) - source_folder = SourceFolder().gnome_shell - self.theme = GlobalTheme(self.colors, source_folder, - config.global_gnome_shell_theme, config.gnome_shell_gresource, - gdm_temp, mode=self.args.mode, is_filled=self.args.filled) - - def _install_theme(self, hue, theme_name, sat): - self.theme.install(hue, sat) + gdm_builder = GdmBuilder(self.colors) + gdm_builder.with_mode(self.args.mode) + gdm_builder.with_filled(self.args.filled) + self.theme = gdm_builder.build() def _apply_tweaks_to_theme(self): for theme in self.theme.themes: diff --git a/scripts/install/local_theme_installer.py b/scripts/install/local_theme_installer.py index d91265a..b181843 100644 --- a/scripts/install/local_theme_installer.py +++ b/scripts/install/local_theme_installer.py @@ -18,9 +18,6 @@ class LocalThemeInstaller(ThemeInstaller): theme_builder.filled(self.args.filled) self.theme = theme_builder.build() - def _install_theme(self, hue, theme_name, sat): - self.theme.install(hue, theme_name, sat) - def _apply_tweaks_to_theme(self): self._apply_tweaks(self.theme) diff --git a/scripts/install/theme_installer.py b/scripts/install/theme_installer.py index 7546802..5ad3c06 100644 --- a/scripts/install/theme_installer.py +++ b/scripts/install/theme_installer.py @@ -3,13 +3,13 @@ import concurrent.futures from abc import ABC, abstractmethod from scripts.install.colors_definer import ColorsDefiner -from scripts.utils.theme.theme import Theme +from scripts.types.theme_base import ThemeBase from scripts.tweaks_manager import TweaksManager class ThemeInstaller(ABC): """Base class for theme installers""" - theme: Theme + theme: ThemeBase def __init__(self, args: argparse.Namespace, colors: ColorsDefiner): self.args = args @@ -37,11 +37,6 @@ class ThemeInstaller(ABC): """Should apply the tweaks for prepared theme""" pass - @abstractmethod - def _install_theme(self, hue, theme_name, sat): - """Should say how to install the defined theme""" - pass - @abstractmethod def _after_install(self): """Method to be called after the theme is installed. Can be used for logging or other actions""" @@ -53,16 +48,14 @@ class ThemeInstaller(ABC): tweaks_manager.apply_tweaks(self.args, theme, self.colors) def _apply_colors(self): - installed_any = False - if self.args.hue: - installed_any = True self._apply_custom_color() - else: - installed_any = self._apply_default_color() + return - if not installed_any: - raise Exception('No color arguments specified. Use -h or --help to see the available options.') + if self._apply_default_color(): + return + + raise Exception('No color arguments specified. Use -h or --help to see the available options.') def _apply_custom_color(self): name = self.args.name @@ -70,7 +63,7 @@ class ThemeInstaller(ABC): sat = self.args.sat theme_name = name if name else f'hue{hue}' - self._install_theme(hue, theme_name, sat) + self.theme.install(hue, theme_name, sat) def _apply_default_color(self) -> bool: colors = self.colors.colors @@ -90,7 +83,7 @@ class ThemeInstaller(ABC): def _run_concurrent_installation(self, colors_to_install): with concurrent.futures.ThreadPoolExecutor() as executor: - futures = [executor.submit(self._install_theme, hue, color, sat) + futures = [executor.submit(self.theme.install, hue, color, sat) for hue, color, sat in colors_to_install] for future in concurrent.futures.as_completed(futures): diff --git a/scripts/types/theme_base.py b/scripts/types/theme_base.py index 54d104f..a9c18c9 100644 --- a/scripts/types/theme_base.py +++ b/scripts/types/theme_base.py @@ -1,12 +1,12 @@ -from abc import ABC +from abc import ABC, abstractmethod class ThemeBase(ABC): """Base class for theme installation and preparation.""" - @staticmethod + @abstractmethod def prepare(self): pass - @staticmethod - def install(self, hue: int, sat: float | None = None): + @abstractmethod + def install(self, hue: int, name: str, sat: float | None = None): pass \ No newline at end of file diff --git a/scripts/utils/alternatives_updater.py b/scripts/utils/alternatives_updater.py index 1f9928a..1761dce 100644 --- a/scripts/utils/alternatives_updater.py +++ b/scripts/utils/alternatives_updater.py @@ -28,7 +28,7 @@ class AlternativesUpdater: @staticmethod @ubuntu_specific - def install_and_set(link: str, name: str, path: PathString, priority: int = 0): + def install_and_set(link: PathString, name: str, path: PathString, priority: int = 0): AlternativesUpdater.install(link, name, path, priority) AlternativesUpdater.set(name, path) diff --git a/scripts/utils/global_theme/__init__.py b/scripts/utils/global_theme/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/scripts/utils/global_theme/gdm.py b/scripts/utils/global_theme/gdm.py new file mode 100644 index 0000000..6af2608 --- /dev/null +++ b/scripts/utils/global_theme/gdm.py @@ -0,0 +1,88 @@ +from scripts.types.theme_base import ThemeBase +from scripts.utils.global_theme.gdm_installer import GDMThemeInstaller +from scripts.utils.global_theme.gdm_preparer import GDMThemePreparer +from scripts.utils.global_theme.gdm_remover import GDMThemeRemover +from scripts.utils.global_theme.gdm_theme_prepare import GDMThemePrepare + + +class GDMTheme(ThemeBase): + """ + GDM theming module. + + This module provides functionality to prepare, install, and remove GNOME Display Manager themes. + It follows a workflow of: + 1. Preparing themes from existing GDM resources + 2. Installing themes with custom colors/styles + 3. Providing ability to restore original GDM themes + + The main entry point is the GDMTheme class, which orchestrates the entire theme management process. + """ + def __init__(self, preparer: GDMThemePreparer, installer: GDMThemeInstaller, remover: GDMThemeRemover): + """ + :param preparer: GDMThemePreparer instance for preparing themes + :param installer: GDMThemeInstaller instance for installing themes + :param remover: GDMThemeRemover instance for removing themes + """ + self.preparer = preparer + self.installer = installer + self.remover = remover + + self.themes: list[GDMThemePrepare] = [] + + def prepare(self): + """ + Prepare the theme for installation. + + This method: + 1. Checks if a theme is already installed and uses backup if needed + 2. Extracts relevant theme files + 3. Processes them into ready-to-compile GDMThemePrepare objects + + The processed themes are stored in the themes attribute for later use. + """ + if self._is_installed(): + self.preparer.use_backup_as_source() + self.themes = self.preparer.prepare() + + def _is_installed(self) -> bool: + """ + Check if a GDM theme is currently installed. + + This looks for specific markers in the system gresource files + that indicate our theme has been installed. + + :return: True if a custom theme is installed, False otherwise + """ + return self.installer.is_installed() + + def install(self, hue: int, name: str, sat: float | None = None): + """ + Install the prepared theme with specified color adjustments. + + This method: + 1. Compiles theme files with the specified hue and saturation + 2. Creates a backup of the original GDM theme if one doesn't exist + 3. Installs the compiled theme to the system + + :param hue: The hue adjustment (0-360) to apply to the theme + :param name: The name of the theme to be installed. In GDM will only be shown in logger + :param sat: Optional saturation adjustment (0-100) to apply + """ + self.installer.compile(self.themes, hue, name, sat) + + if not self._is_installed(): + self.installer.backup() + + self.installer.install() + + def remove(self): + """ + Remove the installed theme and restore the original GDM theme. + + If no theme is installed, displays a warning message to the user. + This will restore from backup and update GDM alternatives if needed. + """ + if self._is_installed(): + self.remover.remove() + else: + self.remover.warn_not_installed() diff --git a/scripts/utils/global_theme/gdm_builder.py b/scripts/utils/global_theme/gdm_builder.py new file mode 100644 index 0000000..edf87f1 --- /dev/null +++ b/scripts/utils/global_theme/gdm_builder.py @@ -0,0 +1,168 @@ +import os.path +from typing import Optional + +from scripts import config +from scripts.install.colors_definer import ColorsDefiner +from scripts.types.installation_color import InstallationMode +from scripts.utils.alternatives_updater import AlternativesUpdater, PathString +from scripts.utils.command_runner.subprocess_command_runner import SubprocessCommandRunner +from scripts.utils.global_theme.gdm import GDMTheme +from scripts.utils.global_theme.gdm_installer import GDMThemeInstaller +from scripts.utils.global_theme.gdm_preparer import GDMThemePreparer +from scripts.utils.global_theme.gdm_remover import GDMThemeRemover +from scripts.utils.global_theme.ubuntu_alternatives_updater import UbuntuGDMAlternativesUpdater +from scripts.utils.gresource.gresource import Gresource +from scripts.utils.logger.console import Console +from scripts.utils.logger.logger import LoggerFactory +from scripts.utils.theme.gnome_shell_theme_builder import GnomeShellThemeBuilder + + +class GdmBuilder: + """ + Builder class for creating GDMTheme instances with configurable components. + + This class follows the Builder pattern to create a GDMTheme with all necessary + dependencies. Dependencies can be injected via the with_* methods or will be + automatically resolved during build() if not provided. + + Example usage: + builder = GdmBuilder(colors_provider) + theme = builder.with_mode("dark").with_filled(True).build() + """ + def __init__(self, colors_provider: ColorsDefiner): + """ + :param colors_provider: A provider for color definitions. + """ + self.colors_provider: ColorsDefiner = colors_provider + self._temp_folder: PathString = os.path.join(config.temp_folder, config.gdm_folder) + self._mode: Optional[InstallationMode] = None + self._is_filled: bool = False + + self._logger_factory: Optional[LoggerFactory] = None + self._gresource: Optional[Gresource] = None + self._ubuntu_gdm_alternatives_updater: Optional[UbuntuGDMAlternativesUpdater] = None + + self._preparer: Optional[GDMThemePreparer] = None + self._installer: Optional[GDMThemeInstaller] = None + self._remover: Optional[GDMThemeRemover] = None + + def with_mode(self, mode: InstallationMode | None) -> 'GdmBuilder': + """Set the mode for the theme.""" + self._mode = mode + return self + + def with_filled(self, is_filled=True) -> 'GdmBuilder': + """Set the filled state for the theme.""" + self._is_filled = is_filled + return self + + def with_logger_factory(self, logger_factory: LoggerFactory) -> 'GdmBuilder': + """Inject a logger factory for logging purposes.""" + self._logger_factory = logger_factory + return self + + def with_gresource(self, gresource: Gresource) -> 'GdmBuilder': + """Inject a gresource instance for managing gresource files.""" + self._gresource = gresource + return self + + def with_ubuntu_gdm_alternatives_updater(self, alternatives_updater: UbuntuGDMAlternativesUpdater) -> 'GdmBuilder': + """Inject an alternatives updater for managing GDM alternatives.""" + self._ubuntu_gdm_alternatives_updater = alternatives_updater + return self + + def with_preparer(self, preparer: GDMThemePreparer) -> 'GdmBuilder': + """Inject a preparer for preparing the theme.""" + self._preparer = preparer + return self + + def with_installer(self, installer: GDMThemeInstaller) -> 'GdmBuilder': + """Inject an installer for installing the theme.""" + self._installer = installer + return self + + def with_remover(self, remover: GDMThemeRemover) -> 'GdmBuilder': + """Inject a remover for removing the theme.""" + self._remover = remover + return self + + def build(self) -> GDMTheme: + """ + Build the GDMTheme object with the configured components. + + Automatically resolves any dependencies that haven't been explicitly + provided through with_* methods. The order of resolution ensures + that dependencies are created before they're needed. + + :return: A fully configured GDMTheme instance ready for use + """ + self._resolve_logger_factory() + self._resolve_gresource() + self._resolve_ubuntu_gdm_alternatives_updater() + + self._resolve_preparer() + self._resolve_installer() + self._resolve_remover() + + return GDMTheme(self._preparer, self._installer, self._remover) + + def _resolve_logger_factory(self): + """Instantiate a default Console logger if not explicitly provided.""" + if self._logger_factory: return + self._logger_factory = Console() + + def _resolve_gresource(self): + """ + Create a Gresource handler if not explicitly provided. + Uses configuration values for file paths and destinations. + """ + if self._gresource: return + + gresource_file = config.gnome_shell_gresource + temp_folder = os.path.join(self._temp_folder, config.extracted_gdm_folder) + destination = config.global_gnome_shell_theme + runner = SubprocessCommandRunner() + + self._gresource = Gresource( + gresource_file=gresource_file, + temp_folder=temp_folder, + destination=destination, + logger_factory=self._logger_factory, + runner=runner + ) + + def _resolve_ubuntu_gdm_alternatives_updater(self): + """Create an UbuntuGDMAlternativesUpdater if not explicitly provided.""" + if self._ubuntu_gdm_alternatives_updater: return + alternatives_updater = AlternativesUpdater() + self._ubuntu_gdm_alternatives_updater = UbuntuGDMAlternativesUpdater(alternatives_updater) + + def _resolve_preparer(self): + """Create a GDMThemePreparer if not explicitly provided.""" + if self._preparer: return + theme_builder = GnomeShellThemeBuilder(self.colors_provider) + self._preparer = GDMThemePreparer( + temp_folder=self._temp_folder, + default_mode=self._mode, + is_filled=self._is_filled, + gresource=self._gresource, + theme_builder=theme_builder, + logger_factory=self._logger_factory + ) + + def _resolve_installer(self): + """Create a GDMThemeInstaller if not explicitly provided.""" + if self._installer: return + self._installer = GDMThemeInstaller( + gresource=self._gresource, + alternatives_updater=self._ubuntu_gdm_alternatives_updater, + ) + + def _resolve_remover(self): + """Create a GDMThemeRemover if not explicitly provided.""" + if self._remover: return + self._remover = GDMThemeRemover( + gresource=self._gresource, + alternatives_updater=self._ubuntu_gdm_alternatives_updater, + logger_factory=self._logger_factory + ) \ No newline at end of file diff --git a/scripts/utils/global_theme/gdm_installer.py b/scripts/utils/global_theme/gdm_installer.py new file mode 100644 index 0000000..29f5350 --- /dev/null +++ b/scripts/utils/global_theme/gdm_installer.py @@ -0,0 +1,66 @@ +from scripts.utils.global_theme.gdm_theme_prepare import GDMThemePrepare +from scripts.utils.global_theme.ubuntu_alternatives_updater import UbuntuGDMAlternativesUpdater +from scripts.utils.gresource.gresource import Gresource + + +class GDMThemeInstaller: + """ + Handles the installation of GDM themes system-wide. + + This class manages: + - Compiling prepared theme resources into a gresource file + - Creating backups of original system files + - Installing compiled themes via the alternatives system + - Detecting if a theme is already installed + """ + def __init__(self, gresource: Gresource, alternatives_updater: UbuntuGDMAlternativesUpdater): + """ + :param gresource: Handler for gresource operations + :param alternatives_updater: Handler for update-alternatives operations + """ + self.gresource = gresource + self.alternatives_updater = alternatives_updater + + self._is_installed_trigger = "\n/* Marble theme */\n" + + def is_installed(self) -> bool: + """ + Check if the theme is installed + by looking for the trigger in the gresource file. + """ + return self.gresource.has_trigger(self._is_installed_trigger) + + def compile(self, themes: list[GDMThemePrepare], hue: int, color: str, sat: int = None): + """ + Prepares themes for gresource and compiles them. + :param themes: themes to be compiled + :param hue: hue value for the theme + :param color: the color name. in GDM will only be shown in logger + :param sat: saturation value for the theme + """ + self._generate_themes(themes, hue, color, sat) + self.gresource.compile() + + def _generate_themes(self, themes: list[GDMThemePrepare], hue: int, color: str, sat: int = None): + """Generate theme files for further compiling by gresource""" + for theme_prepare in themes: + if theme_prepare.label is not None: + theme_prepare.label_theme() + + theme_prepare.remove_keywords("!important") + theme_prepare.remove_properties("background-color", "color", "box-shadow", "border-radius") + theme_prepare.prepend_source_styles(self._is_installed_trigger) + + theme_prepare.install(hue, color, sat, destination=self.gresource.temp_folder) + + def backup(self): + """Backup the current gresource file.""" + self.gresource.backup() + + def install(self): + """ + Install the theme globally by moving the compiled gresource file to the destination. + Also updates the alternatives for the gdm theme. + """ + self.gresource.move() + self.alternatives_updater.install_and_set() \ No newline at end of file diff --git a/scripts/utils/global_theme/gdm_preparer.py b/scripts/utils/global_theme/gdm_preparer.py new file mode 100644 index 0000000..a585c8f --- /dev/null +++ b/scripts/utils/global_theme/gdm_preparer.py @@ -0,0 +1,88 @@ +import os + +from scripts.utils.global_theme.gdm_theme_prepare import GDMThemePrepare +from scripts.utils.gresource.gresource import Gresource +from scripts.utils.logger.logger import LoggerFactory +from scripts.utils.theme.gnome_shell_theme_builder import GnomeShellThemeBuilder + + +class GDMThemePreparer: + """ + GDM theme preparation module. + + This module contains classes responsible for extracting and processing + GDM theme resources for later compilation and installation. + + The main class, GDMThemePreparer, orchestrates the extraction process + and creates GDMThemePrepare objects representing processable themes. + """ + def __init__(self, temp_folder: str, default_mode: str | None, is_filled: bool, + gresource: Gresource, + theme_builder: GnomeShellThemeBuilder, + logger_factory: LoggerFactory): + """ + :param temp_folder: Temporary folder for extracted theme files + :param default_mode: Default theme mode to use if not specified in CSS filename + :param is_filled: Whether to generate filled (True) or outlined (False) styles + :param gresource: Gresource instance for managing gresource files + :param theme_builder: Theme builder instance for creating themes + """ + self.temp_folder = temp_folder + self.gresource_temp_folder = gresource.temp_folder + + self.default_mode = default_mode + self.is_filled = is_filled + + self.gresource = gresource + self.theme_builder = theme_builder + self.logger_factory = logger_factory + + def use_backup_as_source(self): + """Use backup gresource file for extraction""" + self.gresource.use_backup_gresource() + self.logger_factory.create_logger().info("Using backup gresource file for extraction...") + + def prepare(self) -> list[GDMThemePrepare]: + """ + Extract and prepare GDM themes for processing. + :return: List of prepared theme objects ready for compilation + """ + self.gresource.extract() + return self._find_themes() + + def _find_themes(self) -> list[GDMThemePrepare]: + extracted_files = os.listdir(self.gresource_temp_folder) + allowed_css = {"gnome-shell-dark.css", "gnome-shell-light.css", "gnome-shell.css"} + + themes = [ + self._create_theme(file_name) + for file_name in extracted_files + if file_name in allowed_css + ] + return themes + + def _create_theme(self, file_name: str) -> GDMThemePrepare: + """Helper to create and prepare a theme""" + mode = file_name.split("-")[-1].replace(".css", "") + mode = mode if mode in {"dark", "light"} else self.default_mode + + self._setup_theme_builder(file_name, mode) + + theme = self.theme_builder.build() + theme.prepare() + + theme_file = os.path.join(self.gresource_temp_folder, file_name) + return GDMThemePrepare(theme=theme, theme_file=theme_file, label=mode) + + def _setup_theme_builder(self, file_name: str, mode: str): + self.theme_builder.with_temp_folder(self.temp_folder) + + theme_name = file_name.replace(".css", "") + + (self.theme_builder + .with_temp_folder(self.temp_folder) + .with_theme_name(theme_name) + .with_mode(mode) + .filled(self.is_filled) + .with_logger_factory(self.logger_factory) + .with_reset_dependencies()) \ No newline at end of file diff --git a/scripts/utils/global_theme/gdm_remover.py b/scripts/utils/global_theme/gdm_remover.py new file mode 100644 index 0000000..75f4c62 --- /dev/null +++ b/scripts/utils/global_theme/gdm_remover.py @@ -0,0 +1,65 @@ +from scripts.utils.global_theme.ubuntu_alternatives_updater import UbuntuGDMAlternativesUpdater +from scripts.utils.gresource import GresourceBackupNotFoundError +from scripts.utils.gresource.gresource import Gresource +from scripts.utils.logger.console import Console, Color, Format +from scripts.utils.logger.logger import LoggerFactory + + +class GDMThemeRemover: + """ + Responsible for safely removing installed GDM themes. + + This class handles: + - Restoring original gresource files from backups + - Removing theme alternatives from the system + - Providing feedback about removal status + """ + def __init__(self, + gresource: Gresource, + alternatives_updater: UbuntuGDMAlternativesUpdater, + logger_factory: LoggerFactory): + """ + :param gresource: Handler for gresource operations + :param alternatives_updater: Handler for update-alternatives operations + :param logger_factory: Factory for creating loggers + """ + self.gresource = gresource + self.alternatives_updater = alternatives_updater + self.remover_logger = GDMRemoverLogger(logger_factory) + + def remove(self): + """Restores the gresource backup and removes the alternatives.""" + self.remover_logger.start_removing() + + try: + self.gresource.restore() + self.alternatives_updater.remove() + self.remover_logger.success_removing() + except GresourceBackupNotFoundError: + self.remover_logger.error_removing() + + def warn_not_installed(self): + self.remover_logger.not_installed_warning() + + +class GDMRemoverLogger: + def __init__(self, logger_factory: LoggerFactory): + self.logger_factory = logger_factory + self.removing_line = None + + def start_removing(self): + self.removing_line = self.logger_factory.create_logger() + self.removing_line.update("Theme is installed. Removing...") + + def success_removing(self): + self.removing_line.success("Global theme removed successfully. Restart GDM to apply changes.") + + def error_removing(self): + formatted_shell = Console.format("gnome-shell", color=Color.BLUE, format_type=Format.BOLD) + self.removing_line.error(f"Backup file not found. Try reinstalling {formatted_shell} package.") + + def not_installed_warning(self): + self.logger_factory.create_logger().error( + "Theme is not installed. Nothing to remove.") + self.logger_factory.create_logger().warn( + "If theme is still installed globally, try reinstalling gnome-shell package.") \ No newline at end of file diff --git a/scripts/utils/global_theme/gdm_theme_prepare.py b/scripts/utils/global_theme/gdm_theme_prepare.py new file mode 100644 index 0000000..b261e33 --- /dev/null +++ b/scripts/utils/global_theme/gdm_theme_prepare.py @@ -0,0 +1,66 @@ +from scripts.utils import remove_keywords, remove_properties +from scripts.utils.files_labeler import FilesLabeler +from scripts.utils.theme.theme import Theme + + +class GDMThemePrepare: + """ + Prepares theme files for installation into the GDM system. + + This class handles: + - Theme file labeling for dark/light variants + - CSS property and keyword removal for customization + - Theme installation with color adjustments + """ + def __init__(self, theme: Theme, theme_file: str, label: str = None): + """ + :param theme: The theme object to prepare + :param theme_file: Path to the original decompiled CSS file + :param label: Optional label for the theme (e.g. "dark", "light") + """ + self.theme = theme + self.theme_file = theme_file + self.label = label + + def label_theme(self): + """ + Label the theme files if the label is set. + Also updates references in the theme files. + :raises ValueError: if the label is not set + """ + if self.label is None: + raise ValueError("Label is not set for the theme.") + + files_labeler = FilesLabeler(self.theme.temp_folder, self.theme.main_styles) + files_labeler.append_label(self.label) + + def remove_keywords(self, *args: str): + """Remove specific keywords from the theme file""" + remove_keywords(self.theme_file, *args) + + def remove_properties(self, *args: str): + """Remove specific properties from the theme file""" + remove_properties(self.theme_file, *args) + + def prepend_source_styles(self, trigger: str): + """ + Add source styles and installation trigger to the theme file. + + This adds original theme styles and a marker that identifies + the theme as installed by this application. + + :param trigger: String marker used to identify installed themes + """ + with open(self.theme_file, 'r') as gnome_theme: + gnome_styles = gnome_theme.read() + trigger + self.theme.add_to_start(gnome_styles) + + def install(self, hue: int, color: str, sat: int | None, destination: str): + """ + Install the theme to the specified destination + :param hue: Hue value for the theme + :param color: Color name for the theme + :param sat: Saturation value for the theme + :param destination: Destination folder for the theme + """ + self.theme.install(hue, color, sat, destination=destination) \ No newline at end of file diff --git a/scripts/utils/global_theme/ubuntu_alternatives_updater.py b/scripts/utils/global_theme/ubuntu_alternatives_updater.py new file mode 100644 index 0000000..c067d9a --- /dev/null +++ b/scripts/utils/global_theme/ubuntu_alternatives_updater.py @@ -0,0 +1,65 @@ +import os.path + +from scripts import config +from scripts.utils.alternatives_updater import AlternativesUpdater + + +class UbuntuGDMAlternativesUpdater: + """ + Manages update-alternatives for Ubuntu GDM themes. + + This class handles: + - Creating alternatives for GDM theme files + - Setting installed theme as the active alternative + - Removing theme alternatives during uninstallation + """ + def __init__(self, alternatives_updater: AlternativesUpdater): + """ + :param alternatives_updater: Handler for update-alternatives operations + """ + self.ubuntu_gresource_link = config.ubuntu_gresource_link + self.destination_dir = config.global_gnome_shell_theme + self.destination_file = config.gnome_shell_gresource + + self.alternatives_updater = alternatives_updater + + self._update_gresource_paths() + + def _update_gresource_paths(self): + self.ubuntu_gresource_path = os.path.join(self.destination_dir, self.ubuntu_gresource_link) + self.gnome_gresource_path = os.path.join(self.destination_dir, self.destination_file) + + def with_custom_destination(self, destination_dir: str, destination_file: str): + """Set custom destination directory and file for the theme.""" + self.destination_dir = destination_dir + self.destination_file = destination_file + self._update_gresource_paths() + return self + + def install_and_set(self, priority: int = 0): + """ + Add theme as an alternative and set it as active. + + This creates a system alternative for the GDM theme and + makes it the active selection with the specified priority. + + :param priority: Priority level for the alternative (higher wins in conflicts) + """ + self.alternatives_updater.install_and_set( + link=self.ubuntu_gresource_path, + name=self.ubuntu_gresource_link, + path=self.gnome_gresource_path, + priority=priority + ) + + def remove(self): + """ + Remove the theme alternative from the system. + + This removes the previously installed alternative, allowing + the system to fall back to the default GDM theme. + """ + self.alternatives_updater.remove( + name=self.ubuntu_gresource_link, + path=self.gnome_gresource_path + ) diff --git a/scripts/utils/gresource/gresource.py b/scripts/utils/gresource/gresource.py index 3911c91..c9963c4 100644 --- a/scripts/utils/gresource/gresource.py +++ b/scripts/utils/gresource/gresource.py @@ -1,5 +1,6 @@ import os +from scripts.utils.alternatives_updater import PathString from scripts.utils.command_runner.command_runner import CommandRunner from scripts.utils.gresource.gresource_backuper import GresourceBackuperManager from scripts.utils.gresource.gresource_compiler import GresourceCompiler @@ -12,7 +13,7 @@ class Gresource: """Orchestrator for gresource files.""" def __init__( - self, gresource_file: str, temp_folder: str, destination: str, + self, gresource_file: str, temp_folder: PathString, destination: PathString, logger_factory: LoggerFactory, runner: CommandRunner ): """ @@ -34,6 +35,15 @@ class Gresource: self._backuper = GresourceBackuperManager(self._destination_gresource, logger_factory=self.logger_factory) + def has_trigger(self, trigger: str) -> bool: + """ + Check if the trigger is present in the gresource file. + Used to detect if the theme is already installed. + :param trigger: The trigger to check for. + :return: True if the trigger is found, False otherwise. + """ + return self._backuper.has_trigger(trigger) + def use_backup_gresource(self): self._active_source_gresource = self._backuper.get_backup() return self._active_source_gresource diff --git a/scripts/utils/gresource/gresource_backuper.py b/scripts/utils/gresource/gresource_backuper.py index 4c69de0..04245ec 100644 --- a/scripts/utils/gresource/gresource_backuper.py +++ b/scripts/utils/gresource/gresource_backuper.py @@ -11,6 +11,9 @@ class GresourceBackuperManager: self._backup_file = f"{destination_file}.backup" self._backuper = GresourceBackuper(destination_file, self._backup_file, logger_factory) + def has_trigger(self, trigger: str) -> bool: + return self._backuper.has_trigger(trigger) + def backup(self): self._backuper.backup() @@ -27,6 +30,10 @@ class GresourceBackuper: self.backup_file = backup_file self.logger_factory = logger_factory + def has_trigger(self, trigger: str) -> bool: + with open(self.destination_file, "rb") as f: + return trigger.encode() in f.read() + def get_backup(self) -> str: if not os.path.exists(self.backup_file): raise GresourceBackupNotFoundError(self.backup_file) diff --git a/scripts/utils/theme/gnome_shell_theme_builder.py b/scripts/utils/theme/gnome_shell_theme_builder.py index c686499..25bfbac 100644 --- a/scripts/utils/theme/gnome_shell_theme_builder.py +++ b/scripts/utils/theme/gnome_shell_theme_builder.py @@ -4,6 +4,7 @@ from scripts import config from scripts.install.colors_definer import ColorsDefiner from scripts.utils.color_converter.color_converter_impl import ColorConverterImpl from scripts.utils.logger.console import Console +from scripts.utils.logger.logger import LoggerFactory from scripts.utils.style_manager import StyleManager from scripts.utils.theme.theme import Theme from scripts.utils.theme.theme_color_applier import ThemeColorApplier @@ -38,6 +39,7 @@ class GnomeShellThemeBuilder: self.temp_folder = os.path.join(self._base_temp_folder, self.theme_name) self.main_styles = os.path.join(self.temp_folder, f"{self.theme_name}.css") + self.logger_factory: LoggerFactory | None = None self.preparation: ThemePreparation | None = None self.installer: ThemeInstaller | None = None @@ -66,6 +68,28 @@ class GnomeShellThemeBuilder: self.is_filled = filled return self + def with_logger_factory(self, logger_factory: LoggerFactory | None): + """Inject a logger factory for logging purposes.""" + self.logger_factory = logger_factory + return self + + def with_preparation(self, preparation: ThemePreparation | None): + """Inject a preparation instance for preparing the theme.""" + self.preparation = preparation + return self + + def with_installer(self, installer: ThemeInstaller | None): + """Inject an installer for installing the theme.""" + self.installer = installer + return self + + + def with_reset_dependencies(self): + """Reset the dependencies for the theme preparation and installation.""" + self.preparation = None + self.installer = None + return self + def build(self) -> "Theme": """ Constructs and returns a Theme instance using the configured properties. @@ -81,7 +105,7 @@ class GnomeShellThemeBuilder: return Theme(self.preparation, self.installer, self.mode, self.is_filled) def _resolve_preparation(self): - if self.preparation is not None: return + if self.preparation: return file_manager = ThemeTempManager(self.temp_folder) style_manager = StyleManager(self.main_styles) @@ -89,13 +113,14 @@ class GnomeShellThemeBuilder: file_manager=file_manager, style_manager=style_manager) def _resolve_installer(self): - if self.installer is not None: return + if self.installer: return color_converter = ColorConverterImpl() color_replacement_generator = ColorReplacementGenerator( - colors_provider=self.colors_provider, color_converter=color_converter) + colors_provider=self.colors_provider, + color_converter=color_converter) color_applier = ThemeColorApplier(color_replacement_generator=color_replacement_generator) - logger_factory = Console() + logger_factory = self.logger_factory or Console() path_provider = ThemePathProvider() self.installer = ThemeInstaller(self.theme_name, self.temp_folder, self.destination_folder, logger_factory=logger_factory,