mirror of
https://github.com/imarkoff/Marble-shell-theme.git
synced 2025-09-18 01:07:55 -07:00
Refactored Theme class to correspond SOLID patterns
This commit is contained in:
@@ -1,16 +1,14 @@
|
||||
import os.path
|
||||
import os
|
||||
from tempfile import gettempdir
|
||||
|
||||
# folder definitions
|
||||
temp_folder = f"{gettempdir()}/marble"
|
||||
temp_tests_folder = f"{temp_folder}/tests"
|
||||
marble_folder = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
temp_folder = os.path.join(gettempdir(), 'marble')
|
||||
temp_tests_folder = os.path.join(temp_folder, 'tests')
|
||||
gdm_folder = "gdm"
|
||||
gnome_folder = "gnome-shell"
|
||||
temp_gnome_folder = f"{temp_folder}/{gnome_folder}"
|
||||
tweaks_folder = "tweaks"
|
||||
themes_folder = "~/.themes"
|
||||
themes_folder = os.path.expanduser("~/.themes")
|
||||
raw_theme_folder = "theme"
|
||||
scripts_folder = "scripts"
|
||||
|
||||
# GDM definitions
|
||||
global_gnome_shell_theme = "/usr/share/gnome-shell"
|
||||
@@ -19,8 +17,7 @@ ubuntu_gresource_link = "gtk-theme.gresource"
|
||||
extracted_gdm_folder = "theme"
|
||||
|
||||
# files definitions
|
||||
gnome_shell_css = f"{temp_gnome_folder}/gnome-shell.css"
|
||||
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"
|
||||
|
@@ -2,7 +2,8 @@ import os
|
||||
from typing import Optional
|
||||
|
||||
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 . import config
|
||||
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.gresource import GresourceBackupNotFoundError
|
||||
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"""
|
||||
def __init__(self,
|
||||
colors_json: ColorsDefiner, theme_folder: str,
|
||||
destination_folder: str, destination_file: str, temp_folder: str,
|
||||
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 theme_folder: raw theme location
|
||||
: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.theme_folder = theme_folder
|
||||
self.source_folder = source_folder
|
||||
self.destination_folder = destination_folder
|
||||
self.destination_file = destination_file
|
||||
self.temp_folder = temp_folder
|
||||
@@ -75,9 +77,14 @@ class GlobalTheme:
|
||||
|
||||
def __append_theme(self, theme_type: str, mode = None, label: Optional[str] = None):
|
||||
"""Helper to create theme objects"""
|
||||
theme = Theme(theme_type, self.colors_json, self.theme_folder,
|
||||
self.__gresource_temp_folder, self.temp_folder,
|
||||
mode=mode, is_filled=self.is_filled)
|
||||
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))
|
||||
@@ -113,7 +120,7 @@ class GlobalTheme:
|
||||
|
||||
def __add_gnome_styles(self, theme: Theme):
|
||||
"""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
|
||||
theme.add_to_start(gnome_styles)
|
||||
|
||||
|
@@ -4,6 +4,7 @@ from scripts import config
|
||||
from scripts.gdm import GlobalTheme
|
||||
from scripts.install.theme_installer import ThemeInstaller
|
||||
from scripts.utils.logger.console import Console, Color, Format
|
||||
from theme import SourceFolder
|
||||
|
||||
|
||||
class GlobalThemeInstaller(ThemeInstaller):
|
||||
@@ -16,7 +17,8 @@ class GlobalThemeInstaller(ThemeInstaller):
|
||||
|
||||
def _define_theme(self):
|
||||
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,
|
||||
gdm_temp, mode=self.args.mode, is_filled=self.args.filled)
|
||||
|
||||
|
@@ -1,8 +1,6 @@
|
||||
import os.path
|
||||
|
||||
from scripts import config
|
||||
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.logger.console import Console, Color, Format
|
||||
|
||||
@@ -15,10 +13,10 @@ class LocalThemeInstaller(ThemeInstaller):
|
||||
remove_files(self.args, colors)
|
||||
|
||||
def _define_theme(self):
|
||||
theme_folder = os.path.join(config.raw_theme_folder, config.gnome_folder)
|
||||
self.theme = Theme("gnome-shell", self.colors, theme_folder,
|
||||
config.themes_folder, config.temp_folder,
|
||||
mode=self.args.mode, is_filled=self.args.filled)
|
||||
theme_builder = GnomeShellThemeBuilder(self.colors)
|
||||
theme_builder.with_mode(self.args.mode)
|
||||
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)
|
||||
|
@@ -3,7 +3,7 @@ import concurrent.futures
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
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
|
||||
|
||||
|
||||
|
172
scripts/theme.py
172
scripts/theme.py
@@ -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)
|
0
scripts/types/__init__.py
Normal file
0
scripts/types/__init__.py
Normal file
11
scripts/types/installation_color.py
Normal file
11
scripts/types/installation_color.py
Normal 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]
|
12
scripts/types/theme_base.py
Normal file
12
scripts/types/theme_base.py
Normal 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
|
@@ -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)
|
9
theme/__init__.py
Normal file
9
theme/__init__.py
Normal 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")
|
@@ -1,5 +1,6 @@
|
||||
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"
|
||||
|
||||
|
||||
@@ -28,5 +29,5 @@ def apply_tweak(args, theme, colors):
|
||||
theme += ".panel-button,\
|
||||
.clock,\
|
||||
.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))) + ");\
|
||||
}"
|
||||
|
Reference in New Issue
Block a user