Formatted logs

This commit is contained in:
Vladyslav Hroshev
2025-04-01 21:16:28 +03:00
parent e46181e19d
commit 627e5b16ce
8 changed files with 133 additions and 81 deletions

View File

@@ -19,3 +19,5 @@ extracted_gdm_folder = "theme"
gnome_shell_css = f"{temp_gnome_folder}/gnome-shell.css" 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 = "colors.json"
user_themes_extension = "/org/gnome/shell/extensions/user-theme/name"

View File

@@ -4,6 +4,7 @@ import subprocess
from .theme import Theme from .theme import Theme
from .utils import remove_properties, remove_keywords, gnome from .utils import remove_properties, remove_keywords, gnome
from . import config from . import config
from .utils.console import Console, Color, Format
from .utils.files_labeler import FilesLabeler from .utils.files_labeler import FilesLabeler
@@ -69,7 +70,8 @@ class GlobalTheme:
""" """
Extract gresource files to temp folder 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") resources = subprocess.getoutput(f"gresource list {self.gst}").split("\n")
prefix = "/org/gnome/shell/" prefix = "/org/gnome/shell/"
@@ -84,6 +86,8 @@ class GlobalTheme:
with open(output_path, 'wb') as f: with open(output_path, 'wb') as f:
subprocess.run(["gresource", "extract", self.gst, resource], stdout=f, check=True) subprocess.run(["gresource", "extract", self.gst, resource], stdout=f, check=True)
extract_line.success("Extracted gresource files.")
except FileNotFoundError as e: except FileNotFoundError as e:
if "gresource" in str(e): if "gresource" in str(e):
print("Error: 'gresource' command not found.") print("Error: 'gresource' command not found.")
@@ -127,22 +131,16 @@ class GlobalTheme:
def __backup(self): def __backup(self):
"""
Backup installed theme
"""
if self.__is_installed(): if self.__is_installed():
return return
# backup installed theme backup_line = Console.Line()
print("Backing up default theme...")
backup_line.update("Backing up default theme...")
subprocess.run(["cp", "-aT", self.gst, f"{self.gst}.backup"], cwd=self.destination_folder, check=True) 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): def __generate_gresource_xml(self):
"""
Generates.gresource.xml
"""
# list of files to add to gnome-shell-theme.gresource.xml # list of files to add to gnome-shell-theme.gresource.xml
files = [f"<file>{file}</file>" for file in os.listdir(self.extracted_theme)] files = [f"<file>{file}</file>" for file in os.listdir(self.extracted_theme)]
nl = "\n" # fstring doesn't support newline character nl = "\n" # fstring doesn't support newline character
@@ -197,21 +195,23 @@ class GlobalTheme:
gresource_xml.write(generated_xml) gresource_xml.write(generated_xml)
# compile gnome-shell-theme.gresource.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"], subprocess.run(["glib-compile-resources" , f"{self.destination_file}.xml"],
cwd=self.extracted_theme, check=True) cwd=self.extracted_theme, check=True)
compile_line.success("Theme compiled.")
# backup installed theme # backup installed theme
self.__backup() self.__backup()
# install theme # install theme
print("Installing theme...") install_line = Console.Line()
install_line.update("Moving compiled theme to system folder...")
subprocess.run(["sudo", "cp", "-f", subprocess.run(["sudo", "cp", "-f",
f"{self.extracted_theme}/{self.destination_file}", f"{self.extracted_theme}/{self.destination_file}",
f"{self.destination_folder}/{self.destination_file}"], f"{self.destination_folder}/{self.destination_file}"],
check=True) check=True)
install_line.success("Theme moved to system folder.")
print("Theme installed successfully.")
def remove(self): def remove(self):
@@ -221,16 +221,19 @@ class GlobalTheme:
# use backup file if theme is installed # use backup file if theme is installed
if self.__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) backup_path = os.path.join(self.destination_folder, self.backup_file)
dest_path = os.path.join(self.destination_folder, self.destination_file) dest_path = os.path.join(self.destination_folder, self.destination_file)
if os.path.isfile(backup_path): if os.path.isfile(backup_path):
subprocess.run(["sudo", "mv", backup_path, dest_path], check=True) subprocess.run(["sudo", "mv", backup_path, dest_path], check=True)
removing_line.success("Global theme removed successfully. Restart GDM to apply changes.")
else: 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: else:
print("Theme is not installed. Nothing to remove.") Console.Line().error("Theme is not installed. Nothing to remove.")
print("If theme is still installed globally, try reinstalling gnome-shell package.") Console.Line().update("If theme is still installed globally, try reinstalling gnome-shell package.", icon="⚠️")

View File

@@ -3,6 +3,7 @@ import os
from scripts import config 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.console import Console, Color, Format
class GlobalThemeInstaller(ThemeInstaller): class GlobalThemeInstaller(ThemeInstaller):
@@ -27,6 +28,12 @@ class GlobalThemeInstaller(ThemeInstaller):
self._apply_tweaks(theme.theme) self._apply_tweaks(theme.theme)
def _after_install(self): def _after_install(self):
print("\nGDM theme installed successfully.") print()
print("You need to restart gdm.service to apply changes.") Console.Line().update(
print("Run \"systemctl restart gdm.service\" to restart GDM.") 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="🔄")

View File

@@ -4,6 +4,7 @@ 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.theme import Theme
from scripts.utils import remove_files from scripts.utils import remove_files
from scripts.utils.console import Console, Color, Format
class LocalThemeInstaller(ThemeInstaller): class LocalThemeInstaller(ThemeInstaller):
@@ -26,4 +27,6 @@ class LocalThemeInstaller(ThemeInstaller):
self._apply_tweaks(self.theme) self._apply_tweaks(self.theme)
def _after_install(self): def _after_install(self):
print("\nTheme installed successfully.") print()
formatted_output = Console.format("Theme installed successfully.", color=Color.GREEN, format_type=Format.BOLD)
Console.Line().update(formatted_output, icon="🥳")

View File

@@ -11,9 +11,9 @@ class Console:
_next_line = 0 _next_line = 0
class Line: class Line:
def __init__(self, name): def __init__(self, name: Optional[str]=None):
"""Initialize a new managed line""" """Initialize a new managed line"""
self.name = name self.name = name or f"line_{Console._next_line}"
self._reserve_line() self._reserve_line()
def update(self, message, icon=""): def update(self, message, icon=""):
@@ -25,6 +25,9 @@ class Console:
if lines_up > 0: if lines_up > 0:
sys.stdout.write(f"\033[{lines_up}F") sys.stdout.write(f"\033[{lines_up}F")
# Clear line and write status # 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}") sys.stdout.write(f"\r\033[K{icon} {message}")
# Move the cursor back down # Move the cursor back down
if lines_up > 0: if lines_up > 0:
@@ -78,8 +81,8 @@ class Color(Enum):
GRAY = '\033[90m' GRAY = '\033[90m'
@classmethod @classmethod
def get(cls, color: str) -> Optional['Color']: def get(cls, color: str, default: Optional['Color']=None) -> Optional['Color']:
return getattr(cls, color.upper(), None) return getattr(cls, color.upper(), default)
class Format(Enum): class Format(Enum):

View File

@@ -1,10 +1,15 @@
import subprocess 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: def gnome_version() -> str | None:
""" """
Get gnome-shell version Get gnome-shell version
""" """
try: try:
output = subprocess.check_output(['gnome-shell', '--version'], text=True).strip() output = subprocess.check_output(['gnome-shell', '--version'], text=True).strip()
return output.split(' ')[2] return output.split(' ')[2]
@@ -13,21 +18,40 @@ def gnome_version() -> str | None:
def apply_gnome_theme(theme=None) -> bool: def apply_gnome_theme(theme=None) -> bool:
""" """
Apply gnome-shell theme Applies the theme in user theme extension if it is Marble and extension installed.
:param theme: theme name
""" """
try: try:
if theme is None: if theme is None:
current_theme = subprocess.check_output(['dconf', 'read', '/org/gnome/shell/extensions/user-theme/name'], text=True).strip().strip("'") theme = get_current_theme()
if current_theme.startswith("Marble"):
theme = current_theme
else:
return False
subprocess.run(['dconf', 'reset', '/org/gnome/shell/extensions/user-theme/name'], check=True) line = Console.Line("apply_gnome_theme")
subprocess.run(['dconf', 'write', '/org/gnome/shell/extensions/user-theme/name', f"'{theme}'"], check=True) (color, _) = parse_folder(theme)
print(f"Theme '{theme}' applied.") formatted_theme = Console.format(theme, color=Color.get(color, Color.GRAY), format_type=Format.BOLD)
except subprocess.CalledProcessError:
line.update(f"Applying {formatted_theme} theme...")
time.sleep(0.1) # 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 False
return True 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)

View File

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

View File

@@ -7,10 +7,12 @@ import shutil
from collections import defaultdict from collections import defaultdict
from typing import Any from typing import Any
from .console import Console, Color, Format
from .parse_folder import parse_folder
from .. import config from .. import config
import os import os
def remove_files(args: argparse.Namespace, colors: dict[str, Any]): def remove_files(args: argparse.Namespace, formatted_colors: dict[str, Any]):
"""Delete already installed Marble theme""" """Delete already installed Marble theme"""
themes = detect_themes(config.themes_folder) themes = detect_themes(config.themes_folder)
@@ -20,23 +22,26 @@ def remove_files(args: argparse.Namespace, colors: dict[str, Any]):
filtered_themes = themes filtered_themes = themes
if not args.all: if not args.all:
args_dict = vars(args) 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) filtered_themes = themes.filter(arguments)
if not filtered_themes: if not filtered_themes:
print("No matching themes found.") Console.Line().error("No matching themes found.")
return return
colors = [color for (color, modes) in filtered_themes] formatted_colors = [
print(f"The following themes will be deleted: {', '.join(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: 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) filtered_themes.remove(args.mode)
print("Themes deleted successfully.") Console.Line().success("Themes deleted successfully.")
else: else:
print("Operation cancelled.") Console.Line().error("Operation cancelled.")
def detect_themes(path: str) -> 'Themes': def detect_themes(path: str) -> 'Themes':
@@ -59,41 +64,12 @@ def detect_themes(path: str) -> 'Themes':
return 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: class Themes:
"""Collection of themes grouped by color""" """Collection of themes grouped by color"""
def __init__(self): def __init__(self):
self.by_color: dict[str, list[ThemeMode]] = defaultdict(list) # color: list[ThemeMode] 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) self.by_color[color].append(theme_mode)
def filter(self, colors: list[str]): def filter(self, colors: list[str]):
@@ -121,3 +97,26 @@ class Themes:
def __iter__(self): def __iter__(self):
for color, modes in self.by_color.items(): for color, modes in self.by_color.items():
yield color, modes 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}) ")