Refactor GDM module to correspond SOLID principles

- I don't know how I will support this hell ahh code;
- Added some methods to gnome_shell_theme_builder.py;
- Added "color" property "install" method from theme_base.py.
This commit is contained in:
Vladyslav Hroshev
2025-04-12 23:30:34 +03:00
parent 48d10df4b1
commit ca4e4d4cbe
17 changed files with 674 additions and 202 deletions

View File

@@ -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

View File

@@ -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:

View File

@@ -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)

View File

@@ -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):

View File

@@ -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

View File

@@ -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)

View File

View File

@@ -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()

View File

@@ -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
)

View File

@@ -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()

View File

@@ -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())

View File

@@ -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.")

View File

@@ -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)

View File

@@ -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
)

View File

@@ -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

View File

@@ -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)

View File

@@ -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,