Refactored Theme class to correspond SOLID patterns

This commit is contained in:
Vladyslav Hroshev
2025-04-10 18:29:19 +03:00
parent 31e1a3deb9
commit 40a88cb7f4
26 changed files with 534 additions and 252 deletions

View File

@@ -1,16 +1,14 @@
import os.path import os
from tempfile import gettempdir from tempfile import gettempdir
# folder definitions # folder definitions
temp_folder = f"{gettempdir()}/marble" marble_folder = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
temp_tests_folder = f"{temp_folder}/tests" temp_folder = os.path.join(gettempdir(), 'marble')
temp_tests_folder = os.path.join(temp_folder, 'tests')
gdm_folder = "gdm" gdm_folder = "gdm"
gnome_folder = "gnome-shell"
temp_gnome_folder = f"{temp_folder}/{gnome_folder}"
tweaks_folder = "tweaks" tweaks_folder = "tweaks"
themes_folder = "~/.themes" themes_folder = os.path.expanduser("~/.themes")
raw_theme_folder = "theme" raw_theme_folder = "theme"
scripts_folder = "scripts"
# GDM definitions # GDM definitions
global_gnome_shell_theme = "/usr/share/gnome-shell" global_gnome_shell_theme = "/usr/share/gnome-shell"
@@ -19,8 +17,7 @@ ubuntu_gresource_link = "gtk-theme.gresource"
extracted_gdm_folder = "theme" extracted_gdm_folder = "theme"
# files definitions # files definitions
gnome_shell_css = f"{temp_gnome_folder}/gnome-shell.css"
tweak_file = f"./{tweaks_folder}/*/tweak.py" tweak_file = f"./{tweaks_folder}/*/tweak.py"
colors_json = "colors.json" colors_json = os.path.join(marble_folder, "colors.json")
user_themes_extension = "/org/gnome/shell/extensions/user-theme/name" user_themes_extension = "/org/gnome/shell/extensions/user-theme/name"

View File

@@ -2,7 +2,8 @@ import os
from typing import Optional from typing import Optional
from .install.colors_definer import ColorsDefiner from .install.colors_definer import ColorsDefiner
from .theme import Theme from scripts.utils.theme.theme import Theme
from .types.theme_base import ThemeBase
from .utils import remove_properties, remove_keywords from .utils import remove_properties, remove_keywords
from . import config from . import config
from .utils.alternatives_updater import AlternativesUpdater from .utils.alternatives_updater import AlternativesUpdater
@@ -11,25 +12,26 @@ from .utils.command_runner.subprocess_command_runner import SubprocessCommandRun
from .utils.files_labeler import FilesLabeler from .utils.files_labeler import FilesLabeler
from .utils.gresource import GresourceBackupNotFoundError from .utils.gresource import GresourceBackupNotFoundError
from .utils.gresource.gresource import Gresource from .utils.gresource.gresource import Gresource
from .utils.theme.gnome_shell_theme_builder import GnomeShellThemeBuilder
class GlobalTheme: class GlobalTheme(ThemeBase):
"""Class to install global theme for GDM""" """Class to install global theme for GDM"""
def __init__(self, def __init__(self,
colors_json: ColorsDefiner, theme_folder: str, colors_json: ColorsDefiner, source_folder: str,
destination_folder: str, destination_file: str, temp_folder: str, destination_folder: str, destination_file: str, temp_folder: str | bytes,
mode: Optional[str] = None, is_filled = False mode: Optional[str] = None, is_filled = False
): ):
""" """
:param colors_json: location of a JSON file with color values :param colors_json: location of a JSON file with color values
:param theme_folder: raw theme location :param source_folder: raw theme location
:param destination_folder: folder where themes will be installed :param destination_folder: folder where themes will be installed
:param temp_folder: folder where files will be collected :param temp_folder: folder where files will be collected
:param mode: theme mode (light or dark) :param mode: theme mode (light or dark)
:param is_filled: if True, the theme will be filled :param is_filled: if True, the theme will be filled
""" """
self.colors_json = colors_json self.colors_json = colors_json
self.theme_folder = theme_folder self.source_folder = source_folder
self.destination_folder = destination_folder self.destination_folder = destination_folder
self.destination_file = destination_file self.destination_file = destination_file
self.temp_folder = temp_folder self.temp_folder = temp_folder
@@ -75,9 +77,14 @@ class GlobalTheme:
def __append_theme(self, theme_type: str, mode = None, label: Optional[str] = None): def __append_theme(self, theme_type: str, mode = None, label: Optional[str] = None):
"""Helper to create theme objects""" """Helper to create theme objects"""
theme = Theme(theme_type, self.colors_json, self.theme_folder, theme_builder = GnomeShellThemeBuilder(self.colors_json)
self.__gresource_temp_folder, self.temp_folder, theme_builder.with_temp_folder(self.temp_folder)
mode=mode, is_filled=self.is_filled) 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.prepare()
theme_file = os.path.join(self.__gresource_temp_folder, f"{theme_type}.css") 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)) self.themes.append(ThemePrepare(theme=theme, theme_file=theme_file, label=label))
@@ -113,7 +120,7 @@ class GlobalTheme:
def __add_gnome_styles(self, theme: Theme): def __add_gnome_styles(self, theme: Theme):
"""Add gnome styles to the start of the file""" """Add gnome styles to the start of the file"""
with open(f"{theme.destination_folder}/{theme.theme_type}.css", 'r') as gnome_theme: with open(f"{theme.destination_folder}/{theme.theme_name}.css", 'r') as gnome_theme:
gnome_styles = gnome_theme.read() + self.__is_installed_trigger gnome_styles = gnome_theme.read() + self.__is_installed_trigger
theme.add_to_start(gnome_styles) theme.add_to_start(gnome_styles)

View File

@@ -4,6 +4,7 @@ from scripts import config
from scripts.gdm import GlobalTheme from scripts.gdm import GlobalTheme
from scripts.install.theme_installer import ThemeInstaller from scripts.install.theme_installer import ThemeInstaller
from scripts.utils.logger.console import Console, Color, Format from scripts.utils.logger.console import Console, Color, Format
from theme import SourceFolder
class GlobalThemeInstaller(ThemeInstaller): class GlobalThemeInstaller(ThemeInstaller):
@@ -16,7 +17,8 @@ class GlobalThemeInstaller(ThemeInstaller):
def _define_theme(self): def _define_theme(self):
gdm_temp = os.path.join(config.temp_folder, config.gdm_folder) gdm_temp = os.path.join(config.temp_folder, config.gdm_folder)
self.theme = GlobalTheme(self.colors, f"{config.raw_theme_folder}/{config.gnome_folder}", source_folder = SourceFolder().gnome_shell
self.theme = GlobalTheme(self.colors, source_folder,
config.global_gnome_shell_theme, config.gnome_shell_gresource, config.global_gnome_shell_theme, config.gnome_shell_gresource,
gdm_temp, mode=self.args.mode, is_filled=self.args.filled) gdm_temp, mode=self.args.mode, is_filled=self.args.filled)

View File

@@ -1,8 +1,6 @@
import os.path
from scripts import config
from scripts.install.theme_installer import ThemeInstaller from scripts.install.theme_installer import ThemeInstaller
from scripts.theme import Theme from scripts.utils.theme.gnome_shell_theme_builder import GnomeShellThemeBuilder
from scripts.utils.theme.theme import Theme
from scripts.utils import remove_files from scripts.utils import remove_files
from scripts.utils.logger.console import Console, Color, Format from scripts.utils.logger.console import Console, Color, Format
@@ -15,10 +13,10 @@ class LocalThemeInstaller(ThemeInstaller):
remove_files(self.args, colors) remove_files(self.args, colors)
def _define_theme(self): def _define_theme(self):
theme_folder = os.path.join(config.raw_theme_folder, config.gnome_folder) theme_builder = GnomeShellThemeBuilder(self.colors)
self.theme = Theme("gnome-shell", self.colors, theme_folder, theme_builder.with_mode(self.args.mode)
config.themes_folder, config.temp_folder, theme_builder.filled(self.args.filled)
mode=self.args.mode, is_filled=self.args.filled) self.theme = theme_builder.build()
def _install_theme(self, hue, theme_name, sat): def _install_theme(self, hue, theme_name, sat):
self.theme.install(hue, theme_name, sat) self.theme.install(hue, theme_name, sat)

View File

@@ -3,7 +3,7 @@ import concurrent.futures
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from scripts.install.colors_definer import ColorsDefiner from scripts.install.colors_definer import ColorsDefiner
from scripts.theme import Theme from scripts.utils.theme.theme import Theme
from scripts.tweaks_manager import TweaksManager from scripts.tweaks_manager import TweaksManager

View File

@@ -1,172 +0,0 @@
import os
import shutil
import colorsys # colorsys.hls_to_rgb(h, l, s)
from .install.colors_definer import ColorsDefiner
from .utils import (
replace_keywords, # replace keywords in file
copy_files, # copy files from source to destination
destination_return, # copied/modified theme location
generate_file) # combine files from folder to one file
from scripts.utils.logger.console import Console, Color, Format
class Theme:
def __init__(self, theme_type, colors_json, theme_folder, destination_folder, temp_folder,
mode=None, is_filled=False):
"""
Initialize Theme class
:param colors_json: location of a json file with colors
:param theme_type: theme type (gnome-shell, gtk, etc.)
:param theme_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, theme will be filled
"""
self.colors: ColorsDefiner = colors_json
self.temp_folder = f"{temp_folder}/{theme_type}"
self.theme_folder = theme_folder
self.theme_type = theme_type
self.modes = [mode] if mode else ['light', 'dark']
self.destination_folder = destination_folder
self.main_styles = f"{self.temp_folder}/{theme_type}.css"
self.is_filled = is_filled
def __add__(self, other):
"""
Add to main styles another styles
:param other: styles to add
:return: new Theme object
"""
with open(self.main_styles, 'a') as main_styles:
main_styles.write('\n' + other)
return self
def __mul__(self, other):
"""
Copy files to temp folder
:param other: file or folder
:return: new Theme object
"""
if os.path.isfile(other):
shutil.copy(other, self.temp_folder)
else:
shutil.copytree(other, self.temp_folder)
return self
def prepare(self):
# move files to temp folder
copy_files(self.theme_folder, self.temp_folder)
generate_file(f"{self.theme_folder}", self.temp_folder, self.main_styles)
# after generating main styles, remove .css and .versions folders
shutil.rmtree(f"{self.temp_folder}/.css/", ignore_errors=True)
shutil.rmtree(f"{self.temp_folder}/.versions/", ignore_errors=True)
# if theme is filled
if self.is_filled:
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"))
def add_to_start(self, content):
"""
Add content to the start of main styles
:param content: content to add
"""
with open(self.main_styles, 'r') as main_styles:
main_content = main_styles.read()
with open(self.main_styles, 'w') as main_styles:
main_styles.write(content + '\n' + main_content)
def install(self, hue, name: str, sat=None, destination=None):
"""
Copy files and generate theme with specified accent color
:param hue
:param name: theme name
:param sat
:param destination: folder where theme will be installed
"""
joint_modes = f"({', '.join(self.modes)})"
line = Console.Line(name)
formatted_name = Console.format(name.capitalize(), color=Color.get(name), format_type=Format.BOLD)
formatted_mode = Console.format(joint_modes, color=Color.GRAY)
line.update(f"Creating {formatted_name} {formatted_mode} theme...")
try:
self._install_and_apply_theme(hue, name, sat=sat, destination=destination)
line.success(f"{formatted_name} {formatted_mode} theme created successfully.")
except Exception as err:
line.error(f"Error installing {formatted_name} theme: {str(err)}")
def _install_and_apply_theme(self, hue, name, sat=None, destination=None):
is_dest = bool(destination)
for mode in self.modes:
if not is_dest:
destination = destination_return(self.destination_folder, name, mode, self.theme_type)
copy_files(self.temp_folder + '/', destination)
self.__apply_theme(hue, self.temp_folder, destination, mode, sat=sat)
def __apply_theme(self, hue, source, destination, theme_mode, sat=None):
"""
Apply theme to all files in directory
:param hue
:param source
:param destination: file directory
:param theme_mode: theme name (light or dark)
:param sat: color saturation (optional)
"""
for apply_file in os.listdir(f"{source}/"):
self.__apply_colors(hue, destination, theme_mode, apply_file, sat=sat)
def __apply_colors(self, hue, destination, theme_mode, apply_file, sat=None):
"""
Install accent colors from colors.json to different file
:param hue
:param destination: file directory
:param theme_mode: theme name (light or dark)
:param apply_file: file name
:param sat: color saturation (optional)
"""
# list of (keyword, replaced value)
replaced_colors = list()
# colorsys works in range(0, 1)
h = hue / 360
for element in self.colors.replacers:
# if color has default color and hasn't been replaced
if theme_mode not in self.colors.replacers[element] and self.colors.replacers[element]["default"]:
default_element = self.colors.replacers[element]["default"]
default_color = self.colors.replacers[default_element][theme_mode]
self.colors.replacers[element][theme_mode] = default_color
# convert sla to range(0, 1)
lightness = int(self.colors.replacers[element][theme_mode]["l"]) / 100
saturation = int(self.colors.replacers[element][theme_mode]["s"]) / 100 if sat is None else \
int(self.colors.replacers[element][theme_mode]["s"]) * (sat / 100) / 100
alpha = self.colors.replacers[element][theme_mode]["a"]
# convert hsl to rgb and multiply every item
red, green, blue = [int(item * 255) for item in colorsys.hls_to_rgb(h, lightness, saturation)]
replaced_colors.append((element, f"rgba({red}, {green}, {blue}, {alpha})"))
# replace colors
replace_keywords(os.path.expanduser(f"{destination}/{apply_file}"), *replaced_colors)

View File

View File

@@ -0,0 +1,11 @@
from dataclasses import dataclass
from typing import Literal
InstallationMode = Literal["light", "dark"]
@dataclass
class InstallationColor:
hue: int
saturation: int | None
modes: list[InstallationMode]

View File

@@ -0,0 +1,12 @@
from abc import ABC
class ThemeBase(ABC):
"""Base class for theme installation and preparation."""
@staticmethod
def prepare(self):
pass
@staticmethod
def install(self, hue: int, sat: float | None = None):
pass

View File

@@ -1,8 +1,5 @@
from .concatenate_files import concatenate_files
from .copy_files import copy_files from .copy_files import copy_files
from .destinaiton_return import destination_return
from .generate_file import generate_file from .generate_file import generate_file
from .hex_to_rgba import hex_to_rgba
from .remove_files import remove_files from .remove_files import remove_files
from .remove_keywords import remove_keywords from .remove_keywords import remove_keywords
from .remove_properties import remove_properties from .remove_properties import remove_properties

View 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

View File

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

View File

@@ -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}/"

View File

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

View File

@@ -1,4 +1,4 @@
def replace_keywords(file, *args): def replace_keywords(file, *args: tuple[str, str]):
""" """
Replace file with several keywords Replace file with several keywords
:param file: file name where keywords must be replaced :param file: file name where keywords must be replaced

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

View File

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

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

View 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]

View 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}")

View 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}/"

View 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"))

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

9
theme/__init__.py Normal file
View File

@@ -0,0 +1,9 @@
import os
class SourceFolder:
themes_folder = os.path.dirname(__file__)
@property
def gnome_shell(self):
return os.path.join(self.themes_folder, "gnome-shell")

View File

@@ -1,5 +1,6 @@
from scripts import config from scripts import config
from scripts.utils import hex_to_rgba from scripts.utils.color_converter import ColorConverterImpl
panel_folder = f"{config.tweaks_folder}/panel" panel_folder = f"{config.tweaks_folder}/panel"
@@ -28,5 +29,5 @@ def apply_tweak(args, theme, colors):
theme += ".panel-button,\ theme += ".panel-button,\
.clock,\ .clock,\
.clock-display StIcon {\ .clock-display StIcon {\
color: rgba(" + ', '.join(map(str, hex_to_rgba(args.panel_text_color))) + ");\ color: rgba(" + ', '.join(map(str, ColorConverterImpl.hex_to_rgba(args.panel_text_color))) + ");\
}" }"