diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml
index 915509e..52b784b 100644
--- a/.github/workflows/python-app.yml
+++ b/.github/workflows/python-app.yml
@@ -7,11 +7,9 @@ on:
push:
branches:
- "main"
- - "gdm"
pull_request:
branches:
- "main"
- - "gdm"
permissions:
contents: read
@@ -23,10 +21,10 @@ jobs:
steps:
- uses: actions/checkout@v3
- - name: Set up Python 3.10
+ - name: Set up Python 3.13
uses: actions/setup-python@v3
with:
- python-version: "3.10"
+ python-version: "3.13"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
diff --git a/README.md b/README.md
index 2da141a..f9e91e4 100644
--- a/README.md
+++ b/README.md
@@ -43,7 +43,7 @@ Icon theme: https://github.com/vinceliuice/Colloid-icon-theme
## 🚧 Requirements
- GNOME 42-48. Correct functionality on other versions is not guaranteed.
- [User Themes](https://extensions.gnome.org/extension/19/user-themes/ "User Themes") extension.
-- Python 3.2+.
+- Python 3.9+.
## 💡 Installation
diff --git a/install.py b/install.py
index 50b826f..1c2f84c 100644
--- a/install.py
+++ b/install.py
@@ -14,206 +14,35 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-import json # working with json files
-import argparse # command-line options
-import os.path
import shutil
-import textwrap # example text in argparse
-from scripts import config # folder and files definitions
-from scripts.tweaks_manager import TweaksManager # load tweaks from files
-
-from scripts.utils import remove_files # delete already installed Marble theme
-from scripts.utils.gnome import apply_gnome_theme # apply theme to GNOME shell
-
-from scripts.theme import Theme
-from scripts.gdm import GlobalTheme
-
-
-def parse_args(colors) -> argparse.Namespace:
- """
- Parse command-line arguments
- :return: parsed arguments
- """
-
- # script description
- parser = argparse.ArgumentParser(prog="python install.py",
- formatter_class=argparse.RawDescriptionHelpFormatter,
- epilog=textwrap.dedent('''
- Examples:
- -a all accent colors, light & dark mode
- --all --mode dark all accent colors, dark mode
- --purple --mode=light purple accent color, light mode
- --hue 150 --name coldgreen custom coldgreen accent color, light & dark mode
- --red --green --sat=70 red, green accent colors, 70% of stock saturation
- --hue=200 --name=grayblue --sat=50 --mode=dark
- custom grayblue accent color, 50% of stock saturation, dark mode
- '''))
-
- # Default arguments
- parser.add_argument('-r', '--remove', action='store_true', help='remove Marble themes')
- parser.add_argument('-ri', '--reinstall', action='store_true', help='reinstall Marble themes')
-
- default_args = parser.add_argument_group('Install default theme')
- default_args.add_argument('-a', '--all', action='store_true', help='all available accent colors')
-
- for color in colors["colors"]:
- default_args.add_argument(f'--{color}', action='store_true', help=f'{color} theme only')
-
- custom_args = parser.add_argument_group('Install custom color theme')
- custom_args.add_argument('--hue', type=int, choices=range(0, 361), help='generate theme from Hue prompt',
- metavar='(0 - 360)')
- custom_args.add_argument('--name', nargs='?', help='theme name (optional)')
-
- color_styles = parser.add_argument_group("Theme color tweaks")
- color_styles.add_argument("--filled", action="store_true", help="make accent color more vibrant")
-
- color_tweaks = parser.add_argument_group('Optional theme tweaks')
- color_tweaks.add_argument('--mode', choices=['light', 'dark'], help='select a specific theme mode to install')
- color_tweaks.add_argument('--sat', type=int, choices=range(0, 251),
- help='custom color saturation (<100%% - reduce, >100%% - increase)', metavar='(0 - 250)')
-
- gdm_theming = parser.add_argument_group('GDM theming')
- gdm_theming.add_argument('--gdm', action='store_true', help='install GDM theme. \
- Requires root privileges. You must specify a specific color.')
-
- # Dynamically load arguments from each tweak script
- tweaks_manager = TweaksManager()
- tweaks_manager.define_arguments(parser)
-
- return parser.parse_args()
-
-
-def apply_tweaks(args, theme, colors):
- """
- Apply theme tweaks
- :param args: parsed arguments
- :param theme: Theme object
- :param colors: colors from colors.json
- """
-
- tweaks_manager = TweaksManager()
- tweaks_manager.apply_tweaks(args, theme, colors)
-
-
-def install_theme(theme, hue, theme_name, sat, gdm=False):
- """
- Check if GDM and install theme
- :param theme: object to install
- :param hue: color hue
- :param theme_name: future theme name
- :param sat: color saturation
- :param gdm: if GDM theme
- """
-
- if gdm:
- theme.install(hue, sat)
- else:
- theme.install(hue, theme_name, sat)
-
-
-def apply_colors(args, theme, colors, gdm=False):
- """
- Apply accent colors to the theme
- :param args: parsed arguments
- :param theme: Theme object
- :param colors: colors from colors.json
- :param gdm: if GDM theme
- """
-
- is_colors = False # check if any color arguments specified
-
- # if custom color
- if args.hue:
- hue = args.hue
- theme_name = args.name if args.name else f'hue{hue}'
-
- install_theme(theme, hue, theme_name, args.sat, gdm)
- return None
-
- else:
- for color in colors["colors"]:
- if args.all or getattr(args, color):
- is_colors = True
-
- hue = colors["colors"][color]["h"]
- # if saturation already defined in color (gray)
- sat = colors["colors"][color].get("s", args.sat)
-
- install_theme(theme, hue, color, sat, gdm)
- if gdm:
- return None
-
- if not is_colors:
- print('No color arguments specified. Use -h or --help to see the available options.')
- return 1
-
- return None
-
-
-def global_theme(args, colors):
- """
- Apply GDM theme
- :param args: parsed arguments
- :param colors: colors from colors.json
- """
- gdm_temp = os.path.join(config.temp_folder, config.gdm_folder)
- gdm_theme = GlobalTheme(colors, f"{config.raw_theme_folder}/{config.gnome_folder}",
- config.global_gnome_shell_theme, config.gnome_shell_gresource,
- gdm_temp, mode=args.mode, is_filled=args.filled)
-
- if args.remove:
- gdm_rm_status = gdm_theme.remove()
- if gdm_rm_status == 0:
- print("GDM theme removed successfully.")
- return
-
- for theme in gdm_theme.themes:
- apply_tweaks(args, theme.theme, colors)
-
- if apply_colors(args, gdm_theme, colors, gdm=True) is None:
- print("\nGDM theme installed successfully.")
- print("You need to restart gdm.service to apply changes.")
- print("Run \"systemctl restart gdm.service\" to restart GDM.")
-
-
-def local_theme(args, colors):
- """
- Apply local theme
- :param args: parsed arguments
- :param colors: colors from colors.json
- """
-
- if args.remove or args.reinstall:
- remove_files(args, colors["colors"])
- if not args.reinstall:
- return
-
- gnome_shell_theme = Theme("gnome-shell", colors, f"{config.raw_theme_folder}/{config.gnome_folder}",
- config.themes_folder, config.temp_folder,
- mode=args.mode, is_filled=args.filled)
-
- apply_tweaks(args, gnome_shell_theme, colors)
- apply_colors(args, gnome_shell_theme, colors)
+from scripts import config
+from scripts.install import ArgumentsDefiner
+from scripts.install.colors_definer import ColorsDefiner
+from scripts.install.global_theme_installer import GlobalThemeInstaller
+from scripts.install.local_theme_installer import LocalThemeInstaller
+from scripts.utils.gnome import apply_gnome_theme
def main():
- colors = json.load(open(config.colors_json))
+ colors_definer = ColorsDefiner(config.colors_json)
+ args = ArgumentsDefiner(colors_definer.colors).parse()
- args = parse_args(colors)
+ installer_class = GlobalThemeInstaller if args.gdm else LocalThemeInstaller
+ installer = installer_class(args, colors_definer)
- if args.gdm:
- global_theme(args, colors)
+ if args.remove or args.reinstall:
+ installer.remove()
- else:
- local_theme(args, colors)
+ if not args.remove:
+ installer.install()
- if args.remove == args.reinstall:
- apply_gnome_theme()
+ if not args.gdm and not args.remove:
+ apply_gnome_theme()
if __name__ == "__main__":
try:
main()
finally:
- shutil.rmtree(config.temp_folder, ignore_errors=True)
+ shutil.rmtree(config.temp_folder, ignore_errors=True)
\ No newline at end of file
diff --git a/scripts/config.py b/scripts/config.py
index 952fe19..83c9732 100644
--- a/scripts/config.py
+++ b/scripts/config.py
@@ -19,3 +19,5 @@ extracted_gdm_folder = "theme"
gnome_shell_css = f"{temp_gnome_folder}/gnome-shell.css"
tweak_file = f"./{tweaks_folder}/*/tweak.py"
colors_json = "colors.json"
+
+user_themes_extension = "/org/gnome/shell/extensions/user-theme/name"
diff --git a/scripts/gdm.py b/scripts/gdm.py
index fb8b838..4a5104d 100644
--- a/scripts/gdm.py
+++ b/scripts/gdm.py
@@ -2,8 +2,10 @@ import os
import subprocess
from .theme import Theme
-from .utils import label_files, remove_properties, remove_keywords, gnome
+from .utils import remove_properties, remove_keywords, gnome
from . import config
+from .utils.console import Console, Color, Format
+from .utils.files_labeler import FilesLabeler
class ThemePrepare:
@@ -11,7 +13,7 @@ class ThemePrepare:
Theme object prepared for installation
"""
- def __init__(self, theme, theme_file, should_label=False):
+ def __init__(self, theme: Theme, theme_file, should_label=False):
self.theme = theme
self.theme_file = theme_file
self.should_label = should_label
@@ -38,34 +40,20 @@ class GlobalTheme:
self.backup_file = f"{self.destination_file}.backup"
self.backup_trigger = "\n/* Marble theme */\n" # trigger to check if theme is installed
- self.extracted_theme = os.path.join(self.temp_folder, config.extracted_gdm_folder)
+ self.extracted_theme: str = os.path.join(self.temp_folder, config.extracted_gdm_folder)
self.gst = os.path.join(self.destination_folder, self.destination_file) # use backup file if theme is installed
self.themes: list[ThemePrepare] = []
-
- try:
- gnome_version = gnome.gnome_version()
- gnome_major = gnome_version.split(".")[0]
- if int(gnome_major) >= 44:
- self.themes += [
- self.__create_theme("gnome-shell-light", mode='light', should_label=True, is_filled=is_filled),
- self.__create_theme("gnome-shell-dark", mode='dark', is_filled=is_filled)
- ]
- except Exception as e:
- print(f"Error: {e}")
- print("Using single theme.")
-
- if not self.themes:
- self.themes.append(
- self.__create_theme(
- "gnome-shell", mode=mode if mode else 'dark', is_filled=is_filled))
+ self.is_filled = is_filled
+ self.mode = mode
- def __create_theme(self, theme_type, mode=None, should_label=False, is_filled=False):
+ def __create_theme(self, theme_type: str, mode=None, should_label=False, is_filled=False):
"""Helper to create theme objects"""
theme = Theme(theme_type, self.colors_json, self.theme_folder,
self.extracted_theme, self.temp_folder,
mode=mode, is_filled=is_filled)
+ theme.prepare()
theme_file = os.path.join(self.extracted_theme, f"{theme_type}.css")
return ThemePrepare(theme=theme, theme_file=theme_file, should_label=should_label)
@@ -82,7 +70,8 @@ class GlobalTheme:
"""
Extract gresource files to temp folder
"""
- print("Extracting gresource files...")
+ extract_line = Console.Line()
+ extract_line.update("Extracting gresource files...")
resources = subprocess.getoutput(f"gresource list {self.gst}").split("\n")
prefix = "/org/gnome/shell/"
@@ -97,6 +86,8 @@ class GlobalTheme:
with open(output_path, 'wb') as f:
subprocess.run(["gresource", "extract", self.gst, resource], stdout=f, check=True)
+ extract_line.success("Extracted gresource files.")
+
except FileNotFoundError as e:
if "gresource" in str(e):
print("Error: 'gresource' command not found.")
@@ -117,7 +108,7 @@ class GlobalTheme:
gnome_styles = gnome_theme.read() + self.backup_trigger
theme.add_to_start(gnome_styles)
- def __prepare(self, hue, color, sat=None):
+ def __generate_themes(self, hue, color, sat=None):
"""
Generate theme files for gnome-shell-theme.gresource.xml
:param hue: color hue
@@ -125,35 +116,31 @@ class GlobalTheme:
:param sat: color saturation
"""
- for theme in self.themes:
- if theme.should_label:
- label_files(theme.theme.temp_folder, "light", theme.theme.main_styles)
+ for theme_prepare in self.themes:
+ if theme_prepare.should_label:
+ temp_folder = theme_prepare.theme.temp_folder
+ main_styles = theme_prepare.theme.main_styles
+ FilesLabeler(temp_folder, main_styles).append_label("light")
- remove_keywords(theme.theme_file, "!important")
- remove_properties(theme.theme_file, "background-color", "color", "box-shadow", "border-radius")
+ remove_keywords(theme_prepare.theme_file, "!important")
+ remove_properties(theme_prepare.theme_file, "background-color", "color", "box-shadow", "border-radius")
- self.__add_gnome_styles(theme.theme)
+ self.__add_gnome_styles(theme_prepare.theme)
- theme.theme.install(hue, color, sat, destination=self.extracted_theme)
+ theme_prepare.theme.install(hue, color, sat, destination=self.extracted_theme)
def __backup(self):
- """
- Backup installed theme
- """
-
if self.__is_installed():
return
- # backup installed theme
- print("Backing up default theme...")
+ backup_line = Console.Line()
+
+ backup_line.update("Backing up default theme...")
subprocess.run(["cp", "-aT", self.gst, f"{self.gst}.backup"], cwd=self.destination_folder, check=True)
+ backup_line.success("Backed up default theme.")
def __generate_gresource_xml(self):
- """
- Generates.gresource.xml
- """
-
# list of files to add to gnome-shell-theme.gresource.xml
files = [f"{file}" for file in os.listdir(self.extracted_theme)]
nl = "\n" # fstring doesn't support newline character
@@ -165,6 +152,24 @@ class GlobalTheme:
"""
+ def prepare(self):
+ try:
+ gnome_version = gnome.gnome_version()
+ gnome_major = gnome_version.split(".")[0]
+ if int(gnome_major) >= 44:
+ self.themes += [
+ self.__create_theme("gnome-shell-light", mode='light', should_label=True, is_filled=self.is_filled),
+ self.__create_theme("gnome-shell-dark", mode='dark', is_filled=self.is_filled)
+ ]
+ except Exception as e:
+ print(f"Error: {e}")
+ print("Using single theme.")
+
+ if not self.themes:
+ self.themes.append(
+ self.__create_theme(
+ "gnome-shell", mode=self.mode if self.mode else 'dark', is_filled=self.is_filled))
+
def install(self, hue, sat=None):
"""
Install theme globally
@@ -182,7 +187,7 @@ class GlobalTheme:
self.__extract()
# generate theme files for global theme
- self.__prepare(hue, 'Marble', sat)
+ self.__generate_themes(hue, 'Marble', sat)
# generate gnome-shell-theme.gresource.xml
with open(f"{self.extracted_theme}/{self.destination_file}.xml", 'w') as gresource_xml:
@@ -190,21 +195,23 @@ class GlobalTheme:
gresource_xml.write(generated_xml)
# compile gnome-shell-theme.gresource.xml
- print("Compiling theme...")
+ compile_line = Console.Line()
+ compile_line.update("Compiling gnome-shell theme...")
subprocess.run(["glib-compile-resources" , f"{self.destination_file}.xml"],
cwd=self.extracted_theme, check=True)
+ compile_line.success("Theme compiled.")
# backup installed theme
self.__backup()
# install theme
- print("Installing theme...")
+ install_line = Console.Line()
+ install_line.update("Moving compiled theme to system folder...")
subprocess.run(["sudo", "cp", "-f",
f"{self.extracted_theme}/{self.destination_file}",
f"{self.destination_folder}/{self.destination_file}"],
check=True)
-
- print("Theme installed successfully.")
+ install_line.success("Theme moved to system folder.")
def remove(self):
@@ -214,16 +221,19 @@ class GlobalTheme:
# use backup file if theme is installed
if self.__is_installed():
- print("Theme is installed. Removing...")
+ removing_line = Console.Line()
+ removing_line.update("Theme is installed. Removing...")
backup_path = os.path.join(self.destination_folder, self.backup_file)
dest_path = os.path.join(self.destination_folder, self.destination_file)
if os.path.isfile(backup_path):
subprocess.run(["sudo", "mv", backup_path, dest_path], check=True)
+ removing_line.success("Global theme removed successfully. Restart GDM to apply changes.")
else:
- print("Backup file not found. Try reinstalling gnome-shell package.")
+ formatted_shell = Console.format("gnome-shell", color=Color.BLUE, format_type=Format.BOLD)
+ removing_line.error(f"Backup file not found. Try reinstalling {formatted_shell} package.")
else:
- print("Theme is not installed. Nothing to remove.")
- print("If theme is still installed globally, try reinstalling gnome-shell package.")
+ Console.Line().error("Theme is not installed. Nothing to remove.")
+ Console.Line().update("If theme is still installed globally, try reinstalling gnome-shell package.", icon="⚠️")
diff --git a/scripts/install/__init__.py b/scripts/install/__init__.py
new file mode 100644
index 0000000..71152d8
--- /dev/null
+++ b/scripts/install/__init__.py
@@ -0,0 +1 @@
+from scripts.install.arguments_definer import ArgumentsDefiner
\ No newline at end of file
diff --git a/scripts/install/arguments_definer.py b/scripts/install/arguments_definer.py
new file mode 100644
index 0000000..c30186b
--- /dev/null
+++ b/scripts/install/arguments_definer.py
@@ -0,0 +1,73 @@
+import argparse
+import textwrap
+from typing import Any
+
+from scripts.tweaks_manager import TweaksManager
+
+
+class ArgumentsDefiner:
+ def __init__(self, colors: dict[str, Any]):
+ self._parser = argparse.ArgumentParser(prog="python install.py",
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ epilog=self._get_epilog())
+
+ self._define_default_arguments()
+ self._define_color_arguments(colors)
+ self._define_custom_color_arguments()
+ self._define_theme_styles_arguments()
+ self._define_color_tweaks_arguments()
+ self._define_gdm_arguments()
+ self._define_tweaks_arguments()
+
+ def parse(self) -> argparse.Namespace:
+ return self._parser.parse_args()
+
+ @staticmethod
+ def _get_epilog():
+ return textwrap.dedent('''
+ Examples:
+ -a all accent colors, light & dark mode
+ --all --mode dark all accent colors, dark mode
+ --purple --mode=light purple accent color, light mode
+ --hue 150 --name coldgreen custom coldgreen accent color, light & dark mode
+ --red --green --sat=70 red, green accent colors, 70% of stock saturation
+ --hue=200 --name=grayblue --sat=50 --mode=dark
+ custom grayblue accent color, 50% of stock saturation, dark mode
+ ''')
+
+ def _define_default_arguments(self):
+ self._parser.add_argument('-r', '--remove', action='store_true', help='remove Marble themes')
+ self._parser.add_argument('-ri', '--reinstall', action='store_true', help='reinstall Marble themes')
+
+ def _define_color_arguments(self, colors: dict[str, Any]):
+ default_args = self._parser.add_argument_group('Install default theme')
+ default_args.add_argument('-a', '--all', action='store_true', help='all available accent colors')
+
+ for color in colors:
+ default_args.add_argument(f'--{color}', action='store_true', help=f'{color} theme only')
+
+ def _define_custom_color_arguments(self):
+ custom_args = self._parser.add_argument_group('Install custom color theme')
+ custom_args.add_argument('--hue', type=int, choices=range(0, 361), help='generate theme from Hue prompt',
+ metavar='(0 - 360)')
+ custom_args.add_argument('--name', nargs='?', help='theme name (optional)')
+
+ def _define_theme_styles_arguments(self):
+ color_styles = self._parser.add_argument_group("Theme color styles")
+ color_styles.add_argument("--filled", action="store_true", help="make accent color more vibrant")
+
+ def _define_color_tweaks_arguments(self):
+ color_tweaks = self._parser.add_argument_group('Optional theme tweaks')
+ color_tweaks.add_argument('--mode', choices=['light', 'dark'], help='select a specific theme mode to install')
+ color_tweaks.add_argument('--sat', type=int, choices=range(0, 251),
+ help='custom color saturation (<100%% - reduce, >100%% - increase)',
+ metavar='(0 - 250)')
+
+ def _define_gdm_arguments(self):
+ gdm_theming = self._parser.add_argument_group('GDM theming')
+ gdm_theming.add_argument('--gdm', action='store_true', help='install GDM theme. \
+ Requires root privileges. You must specify a specific color.')
+
+ def _define_tweaks_arguments(self):
+ tweaks_manager = TweaksManager()
+ tweaks_manager.define_arguments(self._parser)
\ No newline at end of file
diff --git a/scripts/install/colors_definer.py b/scripts/install/colors_definer.py
new file mode 100644
index 0000000..1e46755
--- /dev/null
+++ b/scripts/install/colors_definer.py
@@ -0,0 +1,17 @@
+import json
+
+
+class ColorsDefiner:
+ # TODO: Create a class for each replacer
+ replacers: dict[ str, # ACCENT-COLOR
+ dict[ str, # default, light/dark
+ str | dict[str, int] # random string, s/l/a
+ ]
+ ]
+ # TODO: Create a class for each color
+ colors: dict[str, dict[str, int]]
+
+ def __init__(self, filename):
+ colors_dict = json.load(open(filename))
+ self.replacers = colors_dict["elements"]
+ self.colors = colors_dict["colors"]
diff --git a/scripts/install/global_theme_installer.py b/scripts/install/global_theme_installer.py
new file mode 100644
index 0000000..a45a341
--- /dev/null
+++ b/scripts/install/global_theme_installer.py
@@ -0,0 +1,39 @@
+import os
+
+from scripts import config
+from scripts.gdm import GlobalTheme
+from scripts.install.theme_installer import ThemeInstaller
+from scripts.utils.console import Console, Color, Format
+
+
+class GlobalThemeInstaller(ThemeInstaller):
+ theme: GlobalTheme
+
+ def remove(self):
+ gdm_rm_status = self.theme.remove()
+ if gdm_rm_status == 0:
+ print("GDM theme removed successfully.")
+
+ 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}",
+ config.global_gnome_shell_theme, config.gnome_shell_gresource,
+ gdm_temp, mode=self.args.mode, is_filled=self.args.filled)
+
+ def _install_theme(self, hue, theme_name, sat):
+ self.theme.install(hue, sat)
+
+ def _apply_tweaks_to_theme(self):
+ for theme in self.theme.themes:
+ self._apply_tweaks(theme.theme)
+
+ def _after_install(self):
+ print()
+ Console.Line().update(
+ Console.format("GDM theme installed successfully.", color=Color.GREEN, format_type=Format.BOLD),
+ icon="🥳"
+ )
+ Console.Line().update("You need to restart GDM to apply changes.", icon="ℹ️ ")
+
+ formatted_command = Console.format("systemctl restart gdm.service", color=Color.YELLOW, format_type=Format.BOLD)
+ Console.Line().update(f"Run {formatted_command} to restart GDM.", icon="🔄")
\ No newline at end of file
diff --git a/scripts/install/local_theme_installer.py b/scripts/install/local_theme_installer.py
new file mode 100644
index 0000000..7648003
--- /dev/null
+++ b/scripts/install/local_theme_installer.py
@@ -0,0 +1,32 @@
+import os.path
+
+from scripts import config
+from scripts.install.theme_installer import ThemeInstaller
+from scripts.theme import Theme
+from scripts.utils import remove_files
+from scripts.utils.console import Console, Color, Format
+
+
+class LocalThemeInstaller(ThemeInstaller):
+ theme: Theme
+
+ def remove(self):
+ colors = self.colors.colors
+ 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)
+
+ def _install_theme(self, hue, theme_name, sat):
+ self.theme.install(hue, theme_name, sat)
+
+ def _apply_tweaks_to_theme(self):
+ self._apply_tweaks(self.theme)
+
+ def _after_install(self):
+ print()
+ formatted_output = Console.format("Theme installed successfully.", color=Color.GREEN, format_type=Format.BOLD)
+ Console.Line().update(formatted_output, icon="🥳")
\ No newline at end of file
diff --git a/scripts/install/theme_installer.py b/scripts/install/theme_installer.py
new file mode 100644
index 0000000..0e6ae90
--- /dev/null
+++ b/scripts/install/theme_installer.py
@@ -0,0 +1,99 @@
+import argparse
+import concurrent.futures
+from abc import ABC, abstractmethod
+
+from scripts.install.colors_definer import ColorsDefiner
+from scripts.theme import Theme
+from scripts.tweaks_manager import TweaksManager
+
+
+class ThemeInstaller(ABC):
+ """Base class for theme installers"""
+ theme: Theme
+
+ def __init__(self, args: argparse.Namespace, colors: ColorsDefiner):
+ self.args = args
+ self.colors = colors
+ self.stop_after_first_installed_color = False
+ self._define_theme()
+
+ @abstractmethod
+ def remove(self):
+ """Method for removing already installed themes"""
+ pass
+
+ def install(self):
+ self.theme.prepare()
+ self._apply_tweaks_to_theme()
+ self._apply_colors()
+ self._after_install()
+
+ @abstractmethod
+ def _define_theme(self):
+ """Here is the place to define the theme object"""
+ pass
+
+ @abstractmethod
+ def _apply_tweaks_to_theme(self):
+ """Should apply the tweaks for prepared theme"""
+ pass
+
+ @abstractmethod
+ def _install_theme(self, hue, theme_name, sat):
+ """Should say how to install the defined theme"""
+ pass
+
+ @abstractmethod
+ def _after_install(self):
+ """Method to be called after the theme is installed. Can be used for logging or other actions"""
+ pass
+
+ def _apply_tweaks(self, theme):
+ """This method should be called in the _apply_tweaks_to_theme method"""
+ tweaks_manager = TweaksManager()
+ tweaks_manager.apply_tweaks(self.args, theme, self.colors)
+
+ def _apply_colors(self):
+ installed_any = False
+
+ if self.args.hue:
+ installed_any = True
+ self._apply_custom_color()
+ else:
+ installed_any = self._apply_default_color()
+
+ if not installed_any:
+ raise Exception('No color arguments specified. Use -h or --help to see the available options.')
+
+ def _apply_custom_color(self):
+ name = self.args.name
+ hue = self.args.hue
+ sat = self.args.sat
+
+ theme_name = name if name else f'hue{hue}'
+ self._install_theme(hue, theme_name, sat)
+
+ def _apply_default_color(self) -> bool:
+ colors = self.colors.colors
+ args = self.args
+
+ colors_to_install = []
+ for color, values in colors.items():
+ if args.all or getattr(args, color, False):
+ hue = values.get('h')
+ sat = values.get('s', args.sat)
+ colors_to_install.append((hue, color, sat))
+
+ if self.stop_after_first_installed_color:
+ break
+
+ if not colors_to_install:
+ return False
+ self._run_concurrent_installation(colors_to_install)
+ return True
+
+ def _run_concurrent_installation(self, colors_to_install):
+ with concurrent.futures.ThreadPoolExecutor() as executor:
+ futures = [executor.submit(self._install_theme, hue, color, sat)
+ for hue, color, sat in colors_to_install]
+ concurrent.futures.wait(futures)
\ No newline at end of file
diff --git a/scripts/theme.py b/scripts/theme.py
index 709a333..05573de 100644
--- a/scripts/theme.py
+++ b/scripts/theme.py
@@ -2,11 +2,13 @@ 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 .utils.console import Console, Color, Format
class Theme:
@@ -23,31 +25,14 @@ class Theme:
:param is_filled: if True, theme will be filled
"""
- self.colors = colors_json
+ self.colors: ColorsDefiner = colors_json
self.temp_folder = f"{temp_folder}/{theme_type}"
self.theme_folder = theme_folder
self.theme_type = theme_type
- self.mode = [mode] if mode else ['light', 'dark']
+ self.modes = [mode] if mode else ['light', 'dark']
self.destination_folder = destination_folder
self.main_styles = f"{self.temp_folder}/{theme_type}.css"
-
- # 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 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"))
+ self.is_filled = is_filled
def __add__(self, other):
"""
@@ -74,9 +59,81 @@ class Theme:
return self
- def __del__(self):
- # delete temp folder
- shutil.rmtree(self.temp_folder, ignore_errors=True)
+ 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):
"""
@@ -93,18 +150,18 @@ class Theme:
# colorsys works in range(0, 1)
h = hue / 360
- for element in self.colors["elements"]:
+ for element in self.colors.replacers:
# if color has default color and hasn't been replaced
- if theme_mode not in self.colors["elements"][element] and self.colors["elements"][element]["default"]:
- default_element = self.colors["elements"][element]["default"]
- default_color = self.colors["elements"][default_element][theme_mode]
- self.colors["elements"][element][theme_mode] = default_color
+ 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["elements"][element][theme_mode]["l"]) / 100
- saturation = int(self.colors["elements"][element][theme_mode]["s"]) / 100 if sat is None else \
- int(self.colors["elements"][element][theme_mode]["s"]) * (sat / 100) / 100
- alpha = self.colors["elements"][element][theme_mode]["a"]
+ 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)]
@@ -112,56 +169,4 @@ class Theme:
replaced_colors.append((element, f"rgba({red}, {green}, {blue}, {alpha})"))
# replace colors
- replace_keywords(os.path.expanduser(f"{destination}/{apply_file}"), *replaced_colors)
-
- 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 install(self, hue, name, sat=None, destination=None):
- """
- Copy files and generate theme with different accent color
- :param hue
- :param name: theme name
- :param sat: color saturation (optional)
- :param destination: folder where theme will be installed
- """
-
- is_dest = bool(destination)
-
- print(f"Creating {name} {', '.join(self.mode)} theme...", end=" ")
-
- try:
- for mode in self.mode:
- 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)
-
- except Exception as err:
- print("\nError: " + str(err))
-
- else:
- print("Done.")
-
- 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)
+ replace_keywords(os.path.expanduser(f"{destination}/{apply_file}"), *replaced_colors)
\ No newline at end of file
diff --git a/scripts/utils/__init__.py b/scripts/utils/__init__.py
index 2ae0305..786b174 100644
--- a/scripts/utils/__init__.py
+++ b/scripts/utils/__init__.py
@@ -3,7 +3,6 @@ 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 .label_files import label_files
from .remove_files import remove_files
from .remove_keywords import remove_keywords
from .remove_properties import remove_properties
diff --git a/scripts/utils/console.py b/scripts/utils/console.py
new file mode 100644
index 0000000..af44267
--- /dev/null
+++ b/scripts/utils/console.py
@@ -0,0 +1,95 @@
+import sys
+import threading
+from enum import Enum
+from typing import Optional
+
+
+class Console:
+ """Manages console output for concurrent processes with line tracking"""
+ _print_lock = threading.Lock()
+ _line_mapping = {}
+ _next_line = 0
+
+ class Line:
+ def __init__(self, name: Optional[str]=None):
+ """Initialize a new managed line"""
+ self.name = name or f"line_{Console._next_line}"
+ self._reserve_line()
+
+ def update(self, message, icon="⏳"):
+ """Update the status message for the line"""
+ with Console._print_lock:
+ # Calculate how many lines to move up
+ lines_up = Console._next_line - Console._line_mapping[self.name]
+ # Move the cursor to correct line
+ if lines_up > 0:
+ sys.stdout.write(f"\033[{lines_up}F")
+ # Clear line and write status
+ if icon.strip() == "":
+ sys.stdout.write(f"\r\033[K{message}")
+ else:
+ sys.stdout.write(f"\r\033[K{icon} {message}")
+ # Move the cursor back down
+ if lines_up > 0:
+ sys.stdout.write(f"\033[{lines_up}E")
+ sys.stdout.flush()
+
+ def success(self, message):
+ self.update(message, "✅")
+
+ def error(self, message):
+ self.update(message, "❌")
+
+ def warn(self, message):
+ self.update(message, "⚠️")
+
+ def _reserve_line(self):
+ """Reserve a line for future updates"""
+ with Console._print_lock:
+ line_number = Console._next_line
+ Console._next_line += 1
+ Console._line_mapping[self.name] = line_number
+ sys.stdout.write(f"\n") # Ensure we have a new line
+ sys.stdout.flush()
+ return line_number
+
+ @staticmethod
+ def format(text: str, color: Optional['Color']=None, format_type: Optional['Format']=None):
+ """Apply color and formatting to text"""
+ formatted_text = text
+
+ if color:
+ formatted_text = color.value + formatted_text + Color.NORMAL.value
+ if format_type:
+ formatted_text = format_type.value + formatted_text + Format.NORMAL.value
+
+ return formatted_text
+
+
+class Color(Enum):
+ """ANSI color codes for terminal output"""
+ NORMAL = '\033[0m' # Reset color
+ BLACK = '\033[30m'
+ RED = '\033[31m'
+ GREEN = '\033[32m'
+ YELLOW = '\033[33m'
+ BLUE = '\033[34m'
+ MAGENTA = '\033[35m'
+ PURPLE = '\033[35m'
+ CYAN = '\033[36m'
+ WHITE = '\033[37m'
+ GRAY = '\033[90m'
+
+ @classmethod
+ def get(cls, color: str, default: Optional['Color']=None) -> Optional['Color']:
+ return getattr(cls, color.upper(), default)
+
+
+class Format(Enum):
+ """ANSI formatting codes for terminal output"""
+ NORMAL = '\033[0m' # Reset formatting
+ BOLD = '\033[1m'
+ ITALIC = '\033[3m'
+ UNDERLINE = '\033[4m'
+ BLINK = '\033[5m'
+ REVERSE = '\033[7m'
\ No newline at end of file
diff --git a/scripts/utils/copy_files.py b/scripts/utils/copy_files.py
index 6eb239f..1839fb7 100644
--- a/scripts/utils/copy_files.py
+++ b/scripts/utils/copy_files.py
@@ -1,4 +1,6 @@
import os
+import shutil
+
def copy_files(source, destination):
"""
@@ -7,6 +9,7 @@ def copy_files(source, destination):
:param destination: where files will be pasted
"""
- destination = os.path.expanduser(destination) # expand ~ to /home/user
+ destination = os.path.expanduser(destination)
os.makedirs(destination, exist_ok=True)
- os.system(f"cp -aT {source} {destination}")
+
+ shutil.copytree(source, destination, dirs_exist_ok=True)
diff --git a/scripts/utils/files_labeler.py b/scripts/utils/files_labeler.py
new file mode 100644
index 0000000..ddfcf26
--- /dev/null
+++ b/scripts/utils/files_labeler.py
@@ -0,0 +1,52 @@
+import os
+from typing import Tuple
+
+type LabeledFileGroup = Tuple[str, str]
+
+class FilesLabeler:
+ def __init__(self, directory: str, *args: str):
+ """
+ Initialize the working directory and files to change
+ """
+ self.directory = directory
+ self.files = args
+
+ def append_label(self, label: str):
+ """
+ Append a label to all files in the directory
+ and update references in the files
+ """
+ labeled_files = self._label_files(label)
+ self._update_references(labeled_files)
+
+ def _label_files(self, label: str) -> list[LabeledFileGroup]:
+ labeled_files = []
+ for filename in os.listdir(self.directory):
+ if label in filename: continue
+
+ name, extension = os.path.splitext(filename)
+ new_filename = f"{name}-{label}{extension}"
+
+ old_filepath = os.path.join(self.directory, filename)
+ new_filepath = os.path.join(self.directory, new_filename)
+ os.rename(old_filepath, new_filepath)
+
+ labeled_files.append((filename, new_filename))
+ return labeled_files
+
+ def _update_references(self, labeled_files: list[LabeledFileGroup]):
+ for file_path in self.files:
+ with open(file_path, 'r') as file:
+ file_content = file.read()
+
+ file_content = self._update_references_in_file(file_content, labeled_files)
+
+ with open(file_path, 'w') as file:
+ file.write(file_content)
+
+ @staticmethod
+ def _update_references_in_file(file_content: str, labeled_files: list[LabeledFileGroup]) -> str:
+ replaced_content = file_content
+ for old_name, new_name in labeled_files:
+ replaced_content = replaced_content.replace(old_name, new_name)
+ return replaced_content
\ No newline at end of file
diff --git a/scripts/utils/gnome.py b/scripts/utils/gnome.py
index cd687c3..9448766 100644
--- a/scripts/utils/gnome.py
+++ b/scripts/utils/gnome.py
@@ -1,10 +1,15 @@
import subprocess
+import time
+
+from scripts import config
+from scripts.utils.console import Console, Format, Color
+from scripts.utils.parse_folder import parse_folder
+
def gnome_version() -> str | None:
"""
Get gnome-shell version
"""
-
try:
output = subprocess.check_output(['gnome-shell', '--version'], text=True).strip()
return output.split(' ')[2]
@@ -13,21 +18,40 @@ def gnome_version() -> str | None:
def apply_gnome_theme(theme=None) -> bool:
"""
- Apply gnome-shell theme
- :param theme: theme name
+ Applies the theme in user theme extension if it is Marble and extension installed.
"""
-
try:
if theme is None:
- current_theme = subprocess.check_output(['dconf', 'read', '/org/gnome/shell/extensions/user-theme/name'], text=True).strip().strip("'")
- if current_theme.startswith("Marble"):
- theme = current_theme
- else:
- return False
+ theme = get_current_theme()
- subprocess.run(['dconf', 'reset', '/org/gnome/shell/extensions/user-theme/name'], check=True)
- subprocess.run(['dconf', 'write', '/org/gnome/shell/extensions/user-theme/name', f"'{theme}'"], check=True)
- print(f"Theme '{theme}' applied.")
- except subprocess.CalledProcessError:
+ line = Console.Line("apply_gnome_theme")
+ (color, _) = parse_folder(theme)
+ formatted_theme = Console.format(theme, color=Color.get(color, Color.GRAY), format_type=Format.BOLD)
+
+ line.update(f"Applying {formatted_theme} theme...")
+ time.sleep(0.025) # applying the theme may freeze, so we need to wait a bit
+ apply_user_theme(theme)
+ line.success(f"Theme {formatted_theme} applied.")
+ except Exception:
return False
- return True
\ No newline at end of file
+ return True
+
+
+def get_current_theme() -> str:
+ """
+ Throws an error if theme is not Marble.
+ """
+ try:
+ output = subprocess.check_output(['dconf', 'read', config.user_themes_extension], text=True)
+ output = output.strip().strip("'")
+
+ if not output.startswith("Marble"):
+ raise Exception(f"Theme {output} doesn't appear to be a Marble theme")
+ return output
+ except subprocess.CalledProcessError:
+ raise Exception("User theme extension not found.")
+
+
+def apply_user_theme(theme_name: str):
+ subprocess.run(['dconf', 'reset', config.user_themes_extension], check=True)
+ subprocess.run(['dconf', 'write', config.user_themes_extension, f"'{theme_name}'"], check=True)
\ No newline at end of file
diff --git a/scripts/utils/label_files.py b/scripts/utils/label_files.py
deleted file mode 100644
index 801b4d5..0000000
--- a/scripts/utils/label_files.py
+++ /dev/null
@@ -1,44 +0,0 @@
-import os
-
-def label_files(directory, label, *args):
- """
- Add a label to all files in a directory
- :param directory: folder where files are located
- :param label: label to add
- :param args: files to change links to labeled files
- :return:
- """
-
- # Open all files
- files = [open(file, 'r') for file in args]
- read_files = []
-
- filenames = []
-
- for filename in os.listdir(directory):
- # Skip if the file is already labeled
- if label in filename:
- continue
-
- # Split the filename into name and extension
- name, extension = os.path.splitext(filename)
-
- # Form the new filename and rename the file
- new_filename = f"{name}-{label}{extension}"
- os.rename(os.path.join(directory, filename), os.path.join(directory, new_filename))
-
- filenames.append((filename, new_filename))
-
- # Replace the filename in all files
- for i, file in enumerate(files):
- read_file = file.read()
- read_file.replace(filenames[i][0], filenames[i][1])
- read_files.append(read_file)
- file.close()
-
- write_files = [open(file, 'w') for file in args]
-
- # Write the changes to the files and close them
- for i, file in enumerate(write_files):
- file.write(read_files[i])
- file.close()
diff --git a/scripts/utils/parse_folder.py b/scripts/utils/parse_folder.py
new file mode 100644
index 0000000..e150735
--- /dev/null
+++ b/scripts/utils/parse_folder.py
@@ -0,0 +1,11 @@
+def parse_folder(folder: str) -> tuple[str, str] | None:
+ """Parse a folder name into color and mode"""
+ folder_arr = folder.split("-")
+
+ if len(folder_arr) < 2 or folder_arr[0] != "Marble":
+ return None
+
+ color = "-".join(folder_arr[1:-1])
+ mode = folder_arr[-1]
+
+ return color, mode
\ No newline at end of file
diff --git a/scripts/utils/remove_files.py b/scripts/utils/remove_files.py
index b3fd73b..ffb9173 100644
--- a/scripts/utils/remove_files.py
+++ b/scripts/utils/remove_files.py
@@ -5,11 +5,14 @@
import argparse
import shutil
from collections import defaultdict
+from typing import Any
+from .console import Console, Color, Format
+from .parse_folder import parse_folder
from .. import config
import os
-def remove_files(args: argparse.Namespace, colors: dict[str, str]):
+def remove_files(args: argparse.Namespace, formatted_colors: dict[str, Any]):
"""Delete already installed Marble theme"""
themes = detect_themes(config.themes_folder)
@@ -19,23 +22,26 @@ def remove_files(args: argparse.Namespace, colors: dict[str, str]):
filtered_themes = themes
if not args.all:
args_dict = vars(args)
- arguments = [color for color in colors.keys() if args_dict.get(color)]
+ arguments = [color for color in formatted_colors.keys() if args_dict.get(color)]
filtered_themes = themes.filter(arguments)
if not filtered_themes:
- print("No matching themes found.")
+ Console.Line().error("No matching themes found.")
return
- colors = [color for (color, modes) in filtered_themes]
- print(f"The following themes will be deleted: {', '.join(colors)}.")
+ formatted_colors = [
+ Console.format(color, color=Color.get(color), format_type=Format.BOLD)
+ for (color, modes) in filtered_themes
+ ]
+ Console.Line().warn(f"The following themes will be deleted: {', '.join(formatted_colors)}.")
if args.mode:
- print(f"Theme modes to be deleted: {args.mode}.")
+ Console.Line().warn(f"Theme modes to be deleted: {args.mode}.")
- if input(f"Proceed? (y/N) ").lower() == "y":
+ if proceed_input().lower() == "y":
filtered_themes.remove(args.mode)
- print("Themes deleted successfully.")
+ Console.Line().success("Themes deleted successfully.")
else:
- print("Operation cancelled.")
+ Console.Line().error("Operation cancelled.")
def detect_themes(path: str) -> 'Themes':
@@ -58,41 +64,12 @@ def detect_themes(path: str) -> 'Themes':
return themes
-def parse_folder(folder: str) -> tuple[str, str] | None:
- """Parse a folder name into color and mode"""
- folder_arr = folder.split("-")
-
- if len(folder_arr) < 2 or folder_arr[0] != "Marble":
- return None
-
- color = "-".join(folder_arr[1:-1])
- mode = folder_arr[-1]
-
- return color, mode
-
-
-class ThemeMode:
- """Concrete theme with mode and path"""
- mode: str
- path: str
-
- def __init__(self, mode: str, path: str):
- self.mode = mode
- self.path = path
-
- def remove(self):
- try:
- shutil.rmtree(self.path)
- except Exception as e:
- print(f"Error deleting {self.path}: {e}")
-
-
class Themes:
"""Collection of themes grouped by color"""
def __init__(self):
self.by_color: dict[str, list[ThemeMode]] = defaultdict(list) # color: list[ThemeMode]
- def add_theme(self, color: str, theme_mode: ThemeMode):
+ def add_theme(self, color: str, theme_mode: 'ThemeMode'):
self.by_color[color].append(theme_mode)
def filter(self, colors: list[str]):
@@ -120,3 +97,26 @@ class Themes:
def __iter__(self):
for color, modes in self.by_color.items():
yield color, modes
+
+
+class ThemeMode:
+ """Concrete theme with mode and path"""
+ mode: str
+ path: str
+
+ def __init__(self, mode: str, path: str):
+ self.mode = mode
+ self.path = path
+
+ def remove(self):
+ try:
+ shutil.rmtree(self.path)
+ except Exception as e:
+ print(f"Error deleting {self.path}: {e}")
+
+
+def proceed_input():
+ formatted_agree = Console.format("y", color=Color.GREEN, format_type=Format.BOLD)
+ formatted_disagree = Console.format("N", color=Color.RED, format_type=Format.BOLD)
+ formatted_proceed = Console.format("Proceed?", format_type=Format.BOLD)
+ return input(f"{formatted_proceed} ({formatted_agree}/{formatted_disagree}) ")
\ No newline at end of file
diff --git a/theme/gnome-shell/.css/messages.css b/theme/gnome-shell/.css/messages.css
index 94031e9..6f89f53 100644
--- a/theme/gnome-shell/.css/messages.css
+++ b/theme/gnome-shell/.css/messages.css
@@ -101,7 +101,7 @@
/* that's much better than adding "margin: $base_padding * 0.5;" to .message-close-button */
.message-close-button { margin: 0; }
.message-header:ltr > :last-child { margin-right: 2px; }
-.messahe-header:rtl > :first-child { margin-left: 2px; }
+.message-header:rtl > :first-child { margin-left: 2px; }
/* close button, expand button (46+) */
.message-close-button,
diff --git a/tweaks/opaque/tweak.py b/tweaks/opaque/tweak.py
index d47af50..232c2ac 100755
--- a/tweaks/opaque/tweak.py
+++ b/tweaks/opaque/tweak.py
@@ -1,9 +1,12 @@
+from scripts.install.colors_definer import ColorsDefiner
+
+
def define_arguments(parser):
color_args = parser.add_argument_group('Color tweaks')
color_args.add_argument('-O', '--opaque', action='store_true', help='make the background in menus/popovers opaque')
-def apply_tweak(args, theme, colors):
+def apply_tweak(args, theme, colors: ColorsDefiner):
if args.opaque:
- colors["elements"]["BACKGROUND-COLOR"]["light"]["a"] = 1
- colors["elements"]["BACKGROUND-COLOR"]["dark"]["a"] = 1
+ colors.replacers["BACKGROUND-COLOR"]["light"]["a"] = 1
+ colors.replacers["BACKGROUND-COLOR"]["dark"]["a"] = 1