mirror of
https://github.com/imarkoff/Marble-shell-theme.git
synced 2025-09-24 20:26:35 -07:00
Refactored Theme class to correspond SOLID patterns
This commit is contained in:
@@ -1,8 +1,5 @@
|
||||
from .concatenate_files import concatenate_files
|
||||
from .copy_files import copy_files
|
||||
from .destinaiton_return import destination_return
|
||||
from .generate_file import generate_file
|
||||
from .hex_to_rgba import hex_to_rgba
|
||||
from .remove_files import remove_files
|
||||
from .remove_keywords import remove_keywords
|
||||
from .remove_properties import remove_properties
|
||||
|
42
scripts/utils/color_converter.py
Normal file
42
scripts/utils/color_converter.py
Normal file
@@ -0,0 +1,42 @@
|
||||
import colorsys
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
|
||||
class ColorConverter(ABC):
|
||||
@staticmethod
|
||||
@abstractmethod
|
||||
def hex_to_rgba(hex_color):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
@abstractmethod
|
||||
def hsl_to_rgb(hue, saturation, lightness):
|
||||
pass
|
||||
|
||||
|
||||
class ColorConverterImpl(ColorConverter):
|
||||
@staticmethod
|
||||
def hex_to_rgba(hex_color):
|
||||
try:
|
||||
if len(hex_color) in range(6, 10):
|
||||
hex_color = hex_color.lstrip('#') + "ff"
|
||||
# if is convertable
|
||||
int(hex_color[:], 16)
|
||||
else:
|
||||
raise ValueError
|
||||
|
||||
except ValueError:
|
||||
raise ValueError(f'Error: Invalid HEX color code: {hex_color}')
|
||||
|
||||
else:
|
||||
return int(hex_color[0:2], 16), \
|
||||
int(hex_color[2:4], 16), \
|
||||
int(hex_color[4:6], 16), \
|
||||
int(hex_color[6:8], 16) / 255
|
||||
|
||||
@staticmethod
|
||||
def hsl_to_rgb(hue, saturation, lightness):
|
||||
# colorsys works in range(0, 1)
|
||||
h = hue / 360
|
||||
red, green, blue = [int(item * 255) for item in colorsys.hls_to_rgb(h, lightness, saturation)]
|
||||
return red, green, blue
|
@@ -1,12 +0,0 @@
|
||||
def concatenate_files(edit_file, file):
|
||||
"""
|
||||
Merge two files
|
||||
:param edit_file: where it will be appended
|
||||
:param file: file you want to append
|
||||
"""
|
||||
|
||||
with open(file, 'r') as read_file:
|
||||
file_content = read_file.read()
|
||||
|
||||
with open(edit_file, 'a') as write_file:
|
||||
write_file.write('\n' + file_content)
|
@@ -1,11 +0,0 @@
|
||||
def destination_return(themes_folder, path_name, theme_mode, theme_type):
|
||||
"""
|
||||
Copied/modified theme location
|
||||
:param themes_folder: themes folder location
|
||||
:param path_name: color name
|
||||
:param theme_mode: theme name (light or dark)
|
||||
:param theme_type: theme type (gnome-shell, gtk-4.0, ...)
|
||||
:return: copied files' folder location
|
||||
"""
|
||||
|
||||
return f"{themes_folder}/Marble-{path_name}-{theme_mode}/{theme_type}/"
|
@@ -1,22 +0,0 @@
|
||||
def hex_to_rgba(hex_color):
|
||||
"""
|
||||
Convert hex(a) to rgba
|
||||
:param hex_color: input value
|
||||
"""
|
||||
|
||||
try:
|
||||
if len(hex_color) in range(6, 10):
|
||||
hex_color = hex_color.lstrip('#') + "ff"
|
||||
# if is convertable
|
||||
int(hex_color[:], 16)
|
||||
else:
|
||||
raise ValueError
|
||||
|
||||
except ValueError:
|
||||
raise ValueError(f'Error: Invalid HEX color code: {hex_color}')
|
||||
|
||||
else:
|
||||
return int(hex_color[0:2], 16), \
|
||||
int(hex_color[2:4], 16), \
|
||||
int(hex_color[4:6], 16), \
|
||||
int(hex_color[6:8], 16) / 255
|
@@ -1,4 +1,4 @@
|
||||
def replace_keywords(file, *args):
|
||||
def replace_keywords(file, *args: tuple[str, str]):
|
||||
"""
|
||||
Replace file with several keywords
|
||||
:param file: file name where keywords must be replaced
|
||||
|
19
scripts/utils/style_manager.py
Normal file
19
scripts/utils/style_manager.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from scripts.utils import generate_file
|
||||
|
||||
|
||||
class StyleManager:
|
||||
def __init__(self, output_file: str):
|
||||
self.output_file = output_file
|
||||
|
||||
def append_content(self, content: str):
|
||||
with open(self.output_file, 'a') as output:
|
||||
output.write(content + '\n')
|
||||
|
||||
def prepend_content(self, content: str):
|
||||
with open(self.output_file, 'r') as output:
|
||||
main_content = output.read()
|
||||
with open(self.output_file, 'w') as output:
|
||||
output.write(content + '\n' + main_content)
|
||||
|
||||
def generate_combined_styles(self, sources_location: str, temp_folder: str):
|
||||
generate_file(sources_location, temp_folder, self.output_file)
|
0
scripts/utils/theme/__init__.py
Normal file
0
scripts/utils/theme/__init__.py
Normal file
96
scripts/utils/theme/gnome_shell_theme_builder.py
Normal file
96
scripts/utils/theme/gnome_shell_theme_builder.py
Normal file
@@ -0,0 +1,96 @@
|
||||
import os.path
|
||||
|
||||
from scripts import config
|
||||
from scripts.install.colors_definer import ColorsDefiner
|
||||
from scripts.utils.color_converter import ColorConverterImpl
|
||||
from scripts.utils.logger.console import Console
|
||||
from scripts.utils.theme.theme import Theme
|
||||
from scripts.utils.theme.theme_color_applier import ColorReplacementGenerator, ThemeColorApplier
|
||||
from scripts.utils.theme.theme_installer import ThemeInstaller
|
||||
from scripts.utils.theme.theme_path_provider import ThemePathProvider
|
||||
from scripts.utils.theme.theme_preparation import ThemePreparation
|
||||
from theme import SourceFolder
|
||||
|
||||
|
||||
class GnomeShellThemeBuilder:
|
||||
"""
|
||||
Builder for creating a Gnome Shell theme.
|
||||
|
||||
Example:
|
||||
builder = GnomeShellThemeBuilder(colors_provider)
|
||||
theme = builder.with_mode("dark").filled().build()
|
||||
theme.prepare()
|
||||
theme.install(hue=200, name="MyTheme")
|
||||
"""
|
||||
|
||||
def __init__(self, colors_provider: ColorsDefiner):
|
||||
self.theme_name = "gnome-shell"
|
||||
self.colors_provider = colors_provider
|
||||
self.source_folder = SourceFolder().gnome_shell
|
||||
self._base_temp_folder = config.temp_folder
|
||||
self.destination_folder = config.themes_folder
|
||||
self.mode = None
|
||||
self.is_filled = False
|
||||
|
||||
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.preparation: ThemePreparation | None = None
|
||||
self.installer: ThemeInstaller | None = None
|
||||
|
||||
def __update_paths(self):
|
||||
"""Update derived paths when base folder or theme name changes"""
|
||||
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")
|
||||
|
||||
def with_temp_folder(self, temp_folder: str):
|
||||
"""Set the base temporary folder"""
|
||||
self._base_temp_folder = temp_folder
|
||||
self.__update_paths()
|
||||
return self
|
||||
|
||||
def with_theme_name(self, theme_name: str):
|
||||
"""Set the theme name"""
|
||||
self.theme_name = theme_name
|
||||
self.__update_paths()
|
||||
return self
|
||||
|
||||
def with_mode(self, mode):
|
||||
self.mode = mode
|
||||
return self
|
||||
|
||||
def filled(self, filled = True):
|
||||
self.is_filled = filled
|
||||
return self
|
||||
|
||||
def build(self) -> "Theme":
|
||||
"""
|
||||
Constructs and returns a Theme instance using the configured properties.
|
||||
|
||||
This method resolves all necessary dependencies for the theme's preparation
|
||||
and installation. The returned Theme will have the mode and filled options set
|
||||
according to the builder's configuration.
|
||||
|
||||
:return: Theme instance ready for preparation and installation
|
||||
"""
|
||||
self._resolve_preparation()
|
||||
self._resolve_installer()
|
||||
return Theme(self.preparation, self.installer, self.mode, self.is_filled)
|
||||
|
||||
def _resolve_preparation(self):
|
||||
if self.preparation is None:
|
||||
self.preparation = ThemePreparation(self.source_folder, self.temp_folder, self.main_styles)
|
||||
|
||||
def _resolve_installer(self):
|
||||
if self.installer is not None: return
|
||||
|
||||
color_converter = ColorConverterImpl()
|
||||
color_replacement_generator = ColorReplacementGenerator(
|
||||
colors_provider=self.colors_provider, color_converter=color_converter)
|
||||
color_applier = ThemeColorApplier(color_replacement_generator=color_replacement_generator)
|
||||
logger_factory = Console()
|
||||
path_provider = ThemePathProvider()
|
||||
self.installer = ThemeInstaller(self.theme_name, self.temp_folder, self.destination_folder,
|
||||
logger_factory=logger_factory,
|
||||
color_applier=color_applier,
|
||||
path_provider=path_provider)
|
93
scripts/utils/theme/theme.py
Normal file
93
scripts/utils/theme/theme.py
Normal file
@@ -0,0 +1,93 @@
|
||||
from scripts.types.installation_color import InstallationColor
|
||||
from scripts.types.theme_base import ThemeBase
|
||||
from scripts.utils.theme.theme_installer import ThemeInstaller
|
||||
from scripts.utils.theme.theme_preparation import ThemePreparation
|
||||
|
||||
|
||||
class Theme(ThemeBase):
|
||||
"""
|
||||
Manages theme preparation and installation.
|
||||
|
||||
The Theme class orchestrates the process of preparing a theme by combining files,
|
||||
applying color schemes, and installing the final theme into a destination folder.
|
||||
"""
|
||||
|
||||
def __init__(self, preparation: ThemePreparation, installer: ThemeInstaller, mode=None, is_filled=False):
|
||||
"""
|
||||
:param preparation: Object responsible for theme extraction and preparation.
|
||||
:param installer: Object responsible for installing the theme.
|
||||
:param mode: Theme mode (e.g., 'light' or 'dark'). If not provided, both modes are used.
|
||||
:param is_filled: if True, theme will be filled
|
||||
"""
|
||||
self.modes = [mode] if mode else ['light', 'dark']
|
||||
self.is_filled = is_filled
|
||||
|
||||
self._preparation = preparation
|
||||
self._installer = installer
|
||||
|
||||
@property
|
||||
def temp_folder(self):
|
||||
"""The temporary folder path where the theme is prepared."""
|
||||
return self._preparation.temp_folder
|
||||
|
||||
@property
|
||||
def destination_folder(self):
|
||||
"""The destination folder path where the theme will be installed."""
|
||||
return self._installer.destination_folder
|
||||
|
||||
@property
|
||||
def main_styles(self):
|
||||
"""The path to the combined styles file generated during preparation."""
|
||||
return self._preparation.combined_styles_location
|
||||
|
||||
@property
|
||||
def theme_name(self):
|
||||
return self._installer.theme_type
|
||||
|
||||
def __add__(self, other: str) -> "Theme":
|
||||
"""
|
||||
Appends additional styles to the main styles file.
|
||||
:param other: The additional styles to append.
|
||||
"""
|
||||
self._preparation += other
|
||||
return self
|
||||
|
||||
def __mul__(self, other: str) -> "Theme":
|
||||
"""
|
||||
Adds a file to the theme, copying it to the temporary folder.
|
||||
:param other: The path of the file or folder to add.
|
||||
"""
|
||||
self._preparation *= other
|
||||
return self
|
||||
|
||||
def add_to_start(self, content) -> "Theme":
|
||||
"""
|
||||
Inserts content at the beginning of the main styles file.
|
||||
:param content: The content to insert.
|
||||
"""
|
||||
self._preparation.add_to_start(content)
|
||||
return self
|
||||
|
||||
def prepare(self):
|
||||
"""Extract theme from source folder and prepare it for installation."""
|
||||
self._preparation.prepare()
|
||||
if self.is_filled:
|
||||
self._preparation.replace_filled_keywords()
|
||||
|
||||
def install(self, hue, name: str, sat: float | None = None, destination: str | None = None):
|
||||
"""
|
||||
Installs the theme by applying the specified accent color and copying the finalized files
|
||||
to the designated destination.
|
||||
|
||||
Args:
|
||||
hue: The hue value for the accent color (0-360 degrees).
|
||||
name: The name of the theme.
|
||||
sat: The saturation value for the accent color.
|
||||
destination: The custom folder where the theme will be installed.
|
||||
"""
|
||||
theme_color = InstallationColor(
|
||||
hue=hue,
|
||||
saturation=sat,
|
||||
modes=self.modes
|
||||
)
|
||||
self._installer.install(theme_color, name, destination)
|
61
scripts/utils/theme/theme_color_applier.py
Normal file
61
scripts/utils/theme/theme_color_applier.py
Normal file
@@ -0,0 +1,61 @@
|
||||
import copy
|
||||
import os
|
||||
|
||||
from scripts.install.colors_definer import ColorsDefiner
|
||||
from scripts.types.installation_color import InstallationColor, InstallationMode
|
||||
from scripts.utils import replace_keywords
|
||||
from scripts.utils.color_converter import ColorConverter
|
||||
|
||||
|
||||
class ThemeColorApplier:
|
||||
"""Class to apply theme colors to files in a directory."""
|
||||
|
||||
def __init__(self, color_replacement_generator: "ColorReplacementGenerator"):
|
||||
self.color_replacement_generator = color_replacement_generator
|
||||
|
||||
def apply(self, theme_color: InstallationColor, destination: str, mode: InstallationMode):
|
||||
"""Apply theme colors to all files in the directory"""
|
||||
replacements = self.color_replacement_generator.convert(mode, theme_color)
|
||||
|
||||
for filename in os.listdir(destination):
|
||||
file_path = os.path.join(destination, filename)
|
||||
replace_keywords(file_path, *replacements)
|
||||
|
||||
|
||||
class ColorReplacementGenerator:
|
||||
def __init__(self, colors_provider: ColorsDefiner, color_converter: ColorConverter):
|
||||
self.colors = copy.deepcopy(colors_provider)
|
||||
self.color_converter = color_converter
|
||||
|
||||
def convert(self, mode: InstallationMode, theme_color: InstallationColor) -> list[tuple[str, str]]:
|
||||
"""Generate a list of color replacements for the given theme color and mode"""
|
||||
return [
|
||||
(element, self._create_rgba_value(element, mode, theme_color))
|
||||
for element in self.colors.replacers
|
||||
]
|
||||
|
||||
def _create_rgba_value(self, element: str, mode: str, theme_color: InstallationColor) -> str:
|
||||
"""Create RGBA value for the specified element"""
|
||||
color_def = self._get_color_definition(element, mode)
|
||||
|
||||
lightness = int(color_def["l"]) / 100
|
||||
saturation = int(color_def["s"]) / 100
|
||||
if theme_color.saturation is not None:
|
||||
saturation *= theme_color.saturation / 100
|
||||
alpha = color_def["a"]
|
||||
|
||||
red, green, blue = self.color_converter.hsl_to_rgb(
|
||||
theme_color.hue, saturation, lightness
|
||||
)
|
||||
|
||||
return f"rgba({red}, {green}, {blue}, {alpha})"
|
||||
|
||||
def _get_color_definition(self, element: str, mode: str) -> dict:
|
||||
"""Get color definition for element, handling defaults if needed"""
|
||||
replacer = self.colors.replacers[element]
|
||||
|
||||
if mode not in replacer and replacer["default"]:
|
||||
default_element = replacer["default"]
|
||||
return self.colors.replacers[default_element][mode]
|
||||
|
||||
return replacer[mode]
|
75
scripts/utils/theme/theme_installer.py
Normal file
75
scripts/utils/theme/theme_installer.py
Normal file
@@ -0,0 +1,75 @@
|
||||
from scripts.types.installation_color import InstallationColor, InstallationMode
|
||||
from scripts.utils import copy_files
|
||||
from scripts.utils.logger.console import Console, Color, Format
|
||||
from scripts.utils.logger.logger import LoggerFactory
|
||||
from scripts.utils.theme.theme_color_applier import ThemeColorApplier
|
||||
from scripts.utils.theme.theme_path_provider import ThemePathProvider
|
||||
|
||||
|
||||
class ThemeInstaller:
|
||||
"""
|
||||
Handles the installation of themes by copying files and applying color schemes.
|
||||
"""
|
||||
|
||||
def __init__(self, theme_type: str, source_folder: str, destination_folder: str,
|
||||
logger_factory: LoggerFactory, color_applier: ThemeColorApplier, path_provider: ThemePathProvider):
|
||||
"""
|
||||
:param theme_type: type of the theme (e.g., gnome-shell, gtk)
|
||||
:param source_folder: folder containing the theme files (e.g. temp folder)
|
||||
:param destination_folder: folder where the theme will be installed
|
||||
"""
|
||||
self.theme_type = theme_type
|
||||
self.source_folder = source_folder
|
||||
self.destination_folder = destination_folder
|
||||
|
||||
self.logger_factory = logger_factory
|
||||
self.color_applier = color_applier
|
||||
self.path_provider = path_provider
|
||||
|
||||
def install(self, theme_color: InstallationColor, name: str, custom_destination: str = None):
|
||||
"""
|
||||
Install theme and generate theme with specified accent color
|
||||
:param theme_color: object containing color and modes
|
||||
:param name: theme name
|
||||
:param custom_destination: optional custom destination folder
|
||||
"""
|
||||
logger = InstallationLogger(name, theme_color.modes, self.logger_factory)
|
||||
|
||||
try:
|
||||
self._perform_installation(theme_color, name, custom_destination=custom_destination)
|
||||
logger.success()
|
||||
except Exception as err:
|
||||
logger.error(str(err))
|
||||
raise
|
||||
|
||||
def _perform_installation(self, theme_color, name, custom_destination=None):
|
||||
for mode in theme_color.modes:
|
||||
destination = (custom_destination or
|
||||
self.path_provider.get_theme_path(
|
||||
self.destination_folder, name, mode, self.theme_type))
|
||||
|
||||
copy_files(self.source_folder, destination)
|
||||
self.color_applier.apply(theme_color, destination, mode)
|
||||
|
||||
|
||||
class InstallationLogger:
|
||||
def __init__(self, name: str, modes: list[InstallationMode], logger_factory: LoggerFactory):
|
||||
self.name = name
|
||||
self.modes = modes
|
||||
|
||||
self.logger = logger_factory.create_logger(self.name)
|
||||
self._setup_logger()
|
||||
|
||||
def _setup_logger(self):
|
||||
self.formatted_name = Console.format(self.name.capitalize(),
|
||||
color=Color.get(self.name),
|
||||
format_type=Format.BOLD)
|
||||
joint_modes = f"({', '.join(self.modes)})"
|
||||
self.formatted_modes = Console.format(joint_modes, color=Color.GRAY)
|
||||
self.logger.update(f"Creating {self.formatted_name} {self.formatted_modes} theme...")
|
||||
|
||||
def success(self):
|
||||
self.logger.success(f"{self.formatted_name} {self.formatted_modes} theme created successfully.")
|
||||
|
||||
def error(self, error_message: str):
|
||||
self.logger.error(f"Error installing {self.formatted_name} theme: {error_message}")
|
4
scripts/utils/theme/theme_path_provider.py
Normal file
4
scripts/utils/theme/theme_path_provider.py
Normal file
@@ -0,0 +1,4 @@
|
||||
class ThemePathProvider:
|
||||
@staticmethod
|
||||
def get_theme_path(themes_folder: str, path_name: str, theme_mode: str, theme_type: str) -> str:
|
||||
return f"{themes_folder}/Marble-{path_name}-{theme_mode}/{theme_type}/"
|
47
scripts/utils/theme/theme_preparation.py
Normal file
47
scripts/utils/theme/theme_preparation.py
Normal file
@@ -0,0 +1,47 @@
|
||||
import os
|
||||
|
||||
from scripts.utils import replace_keywords
|
||||
from scripts.utils.theme.theme_temp_manager import ThemeTempManager
|
||||
from scripts.utils.style_manager import StyleManager
|
||||
|
||||
|
||||
class ThemePreparation:
|
||||
"""
|
||||
Class for extracting themes from the source folder
|
||||
and preparing them for installation.
|
||||
"""
|
||||
|
||||
def __init__(self, sources_location: str, temp_folder: str, combined_styles_location: str):
|
||||
self.sources_location = sources_location
|
||||
self.temp_folder = temp_folder
|
||||
self.combined_styles_location = combined_styles_location
|
||||
|
||||
self.file_manager = ThemeTempManager(temp_folder)
|
||||
self.style_manager = StyleManager(combined_styles_location)
|
||||
|
||||
def __add__(self, content: str) -> "ThemePreparation":
|
||||
self.style_manager.append_content(content)
|
||||
return self
|
||||
|
||||
def __mul__(self, content: str) -> "ThemePreparation":
|
||||
self.file_manager.copy_to_temp(content)
|
||||
return self
|
||||
|
||||
def add_to_start(self, content) -> "ThemePreparation":
|
||||
self.style_manager.prepend_content(content)
|
||||
return self
|
||||
|
||||
def prepare(self):
|
||||
self.file_manager.prepare_files(self.sources_location)
|
||||
self.style_manager.generate_combined_styles(self.sources_location, self.temp_folder)
|
||||
self.file_manager.cleanup()
|
||||
|
||||
def replace_filled_keywords(self):
|
||||
for apply_file in os.listdir(f"{self.temp_folder}/"):
|
||||
replace_keywords(f"{self.temp_folder}/{apply_file}",
|
||||
("BUTTON-COLOR", "ACCENT-FILLED-COLOR"),
|
||||
("BUTTON_HOVER", "ACCENT-FILLED_HOVER"),
|
||||
("BUTTON_ACTIVE", "ACCENT-FILLED_ACTIVE"),
|
||||
("BUTTON_INSENSITIVE", "ACCENT-FILLED_INSENSITIVE"),
|
||||
("BUTTON-TEXT-COLOR", "TEXT-BLACK-COLOR"),
|
||||
("BUTTON-TEXT_SECONDARY", "TEXT-BLACK_SECONDARY"))
|
28
scripts/utils/theme/theme_temp_manager.py
Normal file
28
scripts/utils/theme/theme_temp_manager.py
Normal file
@@ -0,0 +1,28 @@
|
||||
import os
|
||||
import shutil
|
||||
|
||||
from scripts.utils import copy_files
|
||||
|
||||
|
||||
class ThemeTempManager:
|
||||
"""
|
||||
Manages operations with temp folder for Theme class
|
||||
"""
|
||||
def __init__(self, temp_folder: str):
|
||||
self.temp_folder = temp_folder
|
||||
|
||||
def copy_to_temp(self, content: str):
|
||||
if os.path.isfile(content):
|
||||
shutil.copy(content, self.temp_folder)
|
||||
else:
|
||||
shutil.copytree(content, self.temp_folder)
|
||||
return self
|
||||
|
||||
def prepare_files(self, sources_location: str):
|
||||
"""Prepare files in temp folder"""
|
||||
copy_files(sources_location, self.temp_folder)
|
||||
|
||||
def cleanup(self):
|
||||
"""Remove temporary folders"""
|
||||
shutil.rmtree(f"{self.temp_folder}/.css/", ignore_errors=True)
|
||||
shutil.rmtree(f"{self.temp_folder}/.versions/", ignore_errors=True)
|
Reference in New Issue
Block a user