diff --git a/build-aux/flatpak/com.github.GradienceTeam.Gradience.Devel.json b/build-aux/flatpak/com.github.GradienceTeam.Gradience.Devel.json index f5cfc6b8..f6841977 100644 --- a/build-aux/flatpak/com.github.GradienceTeam.Gradience.Devel.json +++ b/build-aux/flatpak/com.github.GradienceTeam.Gradience.Devel.json @@ -14,6 +14,7 @@ "--filesystem=xdg-config/gtk-3.0", "--filesystem=xdg-config/gtk-4.0", "--filesystem=xdg-run/gvfsd", + "--filesystem=xdg-download:ro", "--filesystem=~/.mozilla/firefox", "--filesystem=~/.librewolf", "--filesystem=~/.var/app/org.mozilla.firefox/.mozilla/firefox", diff --git a/build-aux/flatpak/com.github.GradienceTeam.Gradience.json b/build-aux/flatpak/com.github.GradienceTeam.Gradience.json index 719e4049..3b06316d 100644 --- a/build-aux/flatpak/com.github.GradienceTeam.Gradience.json +++ b/build-aux/flatpak/com.github.GradienceTeam.Gradience.json @@ -14,6 +14,7 @@ "--filesystem=xdg-config/gtk-3.0", "--filesystem=xdg-config/gtk-4.0", "--filesystem=xdg-run/gvfsd", + "--filesystem=xdg-download:ro", "--filesystem=~/.mozilla/firefox", "--filesystem=~/.librewolf", "--filesystem=~/.var/app/org.mozilla.firefox/.mozilla/firefox", diff --git a/gradience/backend/flatpak_overrides.py b/gradience/backend/flatpak_overrides.py index 666d9ae9..8d79a81c 100644 --- a/gradience/backend/flatpak_overrides.py +++ b/gradience/backend/flatpak_overrides.py @@ -20,6 +20,8 @@ import os from gi.repository import GLib, Gio, Adw +from gradience.backend.constants import app_id + from gradience.backend.logger import Logger logging = Logger() @@ -32,10 +34,10 @@ class InvalidGTKVersion(Exception): pass -""" Internal helper functions (shouldn't be used outside this file) """ +""" Internal helper functions (shouldn't be used outside this module) """ -def get_system_flatpak_path(): +def __get_system_flatpak_path(): systemPath = GLib.getenv("FLATPAK_SYSTEM_DIR") logging.debug(f"systemPath: {systemPath}") @@ -47,7 +49,7 @@ def get_system_flatpak_path(): return GLib.build_filenamev([systemDataDir, "flatpak"]) -def get_user_flatpak_path(): +def __get_user_flatpak_path(): userPath = GLib.getenv("FLATPAK_USER_DIR") logging.debug(f"userPath: {userPath}") @@ -60,49 +62,196 @@ def get_user_flatpak_path(): return GLib.build_filenamev([userDataDir, "flatpak"]) -def user_save_keyfile(toast_overlay, settings, user_keyfile, filename, gtk_ver): +def __user_save_keyfile(user_keyfile, filename, settings=None, gtk_ver=None, toast_overlay=None): try: user_keyfile.save_to_file(filename) except GLib.GError as e: - toast_overlay.add_toast(Adw.Toast(title=_("Failed to save override"))) + if toast_overlay: + toast_overlay.add_toast(Adw.Toast(title=_("Failed to save override"))) logging.error(f"Failed to save keyfile structure to override. Exc: {e}") else: - if gtk_ver == "gtk4": + if gtk_ver == "gtk4" and settings: settings.set_boolean("user-flatpak-theming-gtk4", True) logging.debug( f"user-flatpak-theming-gtk4: {settings.get_boolean('user-flatpak-theming-gtk4')}" ) - elif gtk_ver == "gtk3": + elif gtk_ver == "gtk3" and settings: settings.set_boolean("user-flatpak-theming-gtk3", True) logging.debug( f"user-flatpak-theming-gtk3: {settings.get_boolean('user-flatpak-theming-gtk3')}" ) + elif not gtk_ver and not settings: + logging.debug("DEV WARNING: 'gtk_ver' and 'settings' parameters aren't set for '__user_save_keyfile' function. Unless you aren't using '{create,remove}_*_override' functions, this is a bug.") -def global_save_keyfile(toast_overlay, settings, global_keyfile, filename, gtk_ver): +def __global_save_keyfile(global_keyfile, filename, settings=None, gtk_ver=None, toast_overlay=None): try: global_keyfile.save_to_file(filename) except GLib.GError as e: - toast_overlay.add_toast(Adw.Toast(title=_("Failed to save override"))) + if toast_overlay: + toast_overlay.add_toast(Adw.Toast(title=_("Failed to save override"))) logging.error(f"Failed to save keyfile structure to override. Exc: {e}") else: - if gtk_ver == "gtk4": + if gtk_ver == "gtk4" and settings: settings.set_boolean("global-flatpak-theming-gtk4", True) logging.debug( f"global-flatpak-theming-gtk4: {settings.get_boolean('global-flatpak-theming-gtk4')}" ) - elif gtk_ver == "gtk3": + elif gtk_ver == "gtk3" and settings: settings.set_boolean("global-flatpak-theming-gtk3", True) logging.debug( f"global-flatpak-theming-gtk3: {settings.get_boolean('global-flatpak-theming-gtk3')}" ) + elif not gtk_ver and not settings: + logging.debug("DEV WARNING: 'gtk_ver' and 'settings' parameters aren't set for '__global_save_keyfile' function. Unless you aren't using '{create,remove}_*_override' functions, this is a bug.") """ Main functions """ -def create_gtk_user_override(toast_overlay, settings, gtk_ver): - override_dir = GLib.build_filenamev([get_user_flatpak_path(), "overrides"]) +def list_file_access(): + override_dir = GLib.build_filenamev([__get_user_flatpak_path(), "overrides"]) + logging.debug(f"override_dir: {override_dir}") + + filename = GLib.build_filenamev([override_dir, app_id]) + + user_keyfile = GLib.KeyFile.new() + + try: + user_keyfile.load_from_file(filename, GLib.KeyFileFlags.NONE) + except GLib.GError as e: + if e.code == 4: + logging.debug("Gradience overrides file doesn't exist") + return False + else: + logging.error(f"Unhandled GLib.FileError error code. Exc: {e}") + raise + else: + try: + filesys_list = user_keyfile.get_string_list( + "Context", "filesystems") + except GLib.GError: + logging.debug("No values in 'filesystems' override") + return False + else: + return filesys_list + + +# TODO: Frontend: Show information to user to relaunch Gradience, as this function modifies \ +# Gradience's overrides. +def allow_file_access(directory, toast_overlay=None): + override_dir = GLib.build_filenamev([__get_user_flatpak_path(), "overrides"]) + logging.debug(f"override_dir: {override_dir}") + + without_access_spec = ( + not ":ro" in directory + and not ":rw" in directory + and not ":create" in directory + ) + + if without_access_spec: + directory += ":ro" + + filename = GLib.build_filenamev([override_dir, app_id]) + + user_keyfile = GLib.KeyFile.new() + + try: + user_keyfile.load_from_file(filename, GLib.KeyFileFlags.NONE) + except GLib.GError as e: + if e.code == 4: + logging.debug("File doesn't exist. Attempting to create one") + if not os.path.exists(override_dir): + try: + dirs = Gio.File.new_for_path(override_dir) + dirs.make_directory_with_parents(None) + except GLib.GError as e: + logging.error(f"Unable to create directories. Exc: {e}") + raise + else: + logging.debug("Directories created.") + + file = Gio.File.new_for_path(filename) + file.create(Gio.FileCreateFlags.NONE, None) + + user_keyfile.load_from_file(filename, GLib.KeyFileFlags.NONE) + user_keyfile.set_string("Context", "filesystems", directory) + + __user_save_keyfile(user_keyfile, filename, + toast_overlay=toast_overlay) + else: + if toast_overlay: + toast_overlay.add_toast( + Adw.Toast(title=_("Unexpected file error occurred")) + ) + logging.error(f"Unhandled GLib.FileError error code. Exc: {e}") + else: + try: + filesys_list = user_keyfile.get_string_list( + "Context", "filesystems") + except GLib.GError: + user_keyfile.set_string("Context", "filesystems", directory) + __user_save_keyfile(user_keyfile, filename, + toast_overlay=toast_overlay) + else: + if directory not in filesys_list: + user_keyfile.set_string_list( + "Context", "filesystems", filesys_list + [directory] + ) + __user_save_keyfile(user_keyfile, filename, + toast_overlay=toast_overlay) + else: + logging.info("Path is already allowed") + + +# TODO: Frontend: Show information to user to relaunch Gradience, as this function modifies \ +# Gradience's overrides. +def disallow_file_access(directory, toast_overlay=None): + override_dir = GLib.build_filenamev([__get_user_flatpak_path(), "overrides"]) + logging.debug(f"override_dir: {override_dir}") + + filename = GLib.build_filenamev([override_dir, app_id]) + + user_keyfile = GLib.KeyFile.new() + + try: + user_keyfile.load_from_file(filename, GLib.KeyFileFlags.NONE) + except GLib.GError as e: + if e.code == 4: + logging.debug("File doesn't exist") + return + else: + if toast_overlay: + toast_overlay.add_toast( + Adw.Toast(title=_("Unexpected file error occurred")) + ) + logging.error(f"Unhandled GLib.FileError error code. Exc: {e}") + raise + else: + try: + filesys_list = user_keyfile.get_string_list( + "Context", "filesystems") + except GLib.GError: + logging.debug("Group/key not found") + return + else: + if directory in filesys_list: + logging.debug(f"before: {filesys_list}") + filesys_list.remove(directory) + logging.debug(f"after: {filesys_list}") + + user_keyfile.set_string_list( + "Context", "filesystems", filesys_list) + __user_save_keyfile(user_keyfile, filename, + toast_overlay=toast_overlay) + logging.debug("Path removed") + else: + logging.debug("Path doesn't exist in overrides") + return + + +def create_gtk_user_override(settings, gtk_ver, toast_overlay=None): + override_dir = GLib.build_filenamev([__get_user_flatpak_path(), "overrides"]) logging.debug(f"override_dir: {override_dir}") filename = GLib.build_filenamev([override_dir, "global"]) @@ -147,12 +296,13 @@ def create_gtk_user_override(toast_overlay, settings, gtk_ver): user_keyfile.load_from_file(filename, GLib.KeyFileFlags.NONE) user_keyfile.set_string("Context", "filesystems", gtk_path) - user_save_keyfile(toast_overlay, settings, - user_keyfile, filename, gtk_ver) + __user_save_keyfile(user_keyfile, filename, + settings, gtk_ver, toast_overlay) else: - toast_overlay.add_toast( - Adw.Toast(title=_("Unexpected file error occurred")) - ) + if toast_overlay: + toast_overlay.add_toast( + Adw.Toast(title=_("Unexpected file error occurred")) + ) logging.error(f"Unhandled GLib.FileError error code. Exc: {e}") else: try: @@ -160,16 +310,15 @@ def create_gtk_user_override(toast_overlay, settings, gtk_ver): "Context", "filesystems") except GLib.GError: user_keyfile.set_string("Context", "filesystems", gtk_path) - user_save_keyfile(toast_overlay, settings, - user_keyfile, filename, gtk_ver) + __user_save_keyfile(user_keyfile, filename, + settings, gtk_ver, toast_overlay) else: if gtk_path not in filesys_list: user_keyfile.set_string_list( "Context", "filesystems", filesys_list + [gtk_path] ) - user_save_keyfile( - toast_overlay, settings, user_keyfile, filename, gtk_ver - ) + __user_save_keyfile(user_keyfile, filename, + settings, gtk_ver, toast_overlay) else: if is_gtk4: settings.set_boolean("user-flatpak-theming-gtk4", True) @@ -178,8 +327,8 @@ def create_gtk_user_override(toast_overlay, settings, gtk_ver): logging.debug("Value already exists.") -def remove_gtk_user_override(toast_overlay, settings, gtk_ver): - override_dir = GLib.build_filenamev([get_user_flatpak_path(), "overrides"]) +def remove_gtk_user_override(settings, gtk_ver, toast_overlay=None): + override_dir = GLib.build_filenamev([__get_user_flatpak_path(), "overrides"]) logging.debug(f"override_dir: {override_dir}") filename = GLib.build_filenamev([override_dir, "global"]) @@ -210,9 +359,10 @@ def remove_gtk_user_override(toast_overlay, settings, gtk_ver): set_theming() logging.warning("remove override: File doesn't exist") else: - toast_overlay.add_toast( - Adw.Toast(title=_("Unexpected file error occurred")) - ) + if toast_overlay: + toast_overlay.add_toast( + Adw.Toast(title=_("Unexpected file error occurred")) + ) logging.error(f"Unhandled GLib.FileError error code. Exc: {e}") else: try: @@ -229,9 +379,8 @@ def remove_gtk_user_override(toast_overlay, settings, gtk_ver): user_keyfile.set_string_list( "Context", "filesystems", filesys_list) - user_save_keyfile( - toast_overlay, settings, user_keyfile, filename, gtk_ver - ) + __user_save_keyfile(user_keyfile, filename, + settings, gtk_ver, toast_overlay) logging.debug("remove override: Value removed.") else: set_theming() @@ -242,9 +391,9 @@ def remove_gtk_user_override(toast_overlay, settings, gtk_ver): # TODO: Implement user authentication using Polkit -def create_gtk_global_override(toast_overlay, settings, gtk_ver): +def create_gtk_global_override(settings, gtk_ver, toast_overlay=None): override_dir = GLib.build_filenamev( - [get_system_flatpak_path(), "overrides"]) + [__get_system_flatpak_path(), "overrides"]) logging.debug(f"override_dir: {override_dir}") filename = GLib.build_filenamev([override_dir, "global"]) @@ -289,13 +438,13 @@ def create_gtk_global_override(toast_overlay, settings, gtk_ver): global_keyfile.load_from_file(filename, GLib.KeyFileFlags.NONE) global_keyfile.set_string("Context", "filesystems", gtk_path) - global_save_keyfile( - toast_overlay, settings, global_keyfile, filename, gtk_ver - ) + __global_save_keyfile(global_keyfile, filename, + settings, gtk_ver, toast_overlay) else: - toast_overlay.add_toast( - Adw.Toast(title=_("Unexpected file error occurred")) - ) + if toast_overlay: + toast_overlay.add_toast( + Adw.Toast(title=_("Unexpected file error occurred")) + ) logging.error(f"Unhandled GLib.FileError error code. Exc: {e}") else: try: @@ -303,17 +452,15 @@ def create_gtk_global_override(toast_overlay, settings, gtk_ver): "Context", "filesystems") except GLib.GError: global_keyfile.set_string("Context", "filesystems", gtk_path) - global_save_keyfile( - toast_overlay, settings, global_keyfile, filename, gtk_ver - ) + __global_save_keyfile(global_keyfile, filename, + settings, gtk_ver, toast_overlay) else: if gtk_path not in filesys_list: global_keyfile.set_string_list( "Context", "filesystems", filesys_list + [gtk_path] ) - global_save_keyfile( - toast_overlay, settings, global_keyfile, filename, gtk_ver - ) + __global_save_keyfile(global_keyfile, filename, + settings, gtk_ver, toast_overlay) else: if is_gtk4: settings.set_boolean("global-flatpak-theming-gtk4", True) @@ -322,9 +469,9 @@ def create_gtk_global_override(toast_overlay, settings, gtk_ver): logging.debug("Value already exists.") -def remove_gtk_global_override(toast_overlay, settings, gtk_ver): +def remove_gtk_global_override(settings, gtk_ver, toast_overlay=None): override_dir = GLib.build_filenamev( - [get_system_flatpak_path(), "overrides"]) + [__get_system_flatpak_path(), "overrides"]) logging.debug(f"override_dir: {override_dir}") filename = GLib.build_filenamev([override_dir, "global"]) @@ -355,9 +502,10 @@ def remove_gtk_global_override(toast_overlay, settings, gtk_ver): set_theming() logging.warning("remove override: File doesn't exist") else: - toast_overlay.add_toast( - Adw.Toast(title=_("Unexpected file error occurred")) - ) + if toast_overlay: + toast_overlay.add_toast( + Adw.Toast(title=_("Unexpected file error occurred")) + ) logging.error(f"Unhandled GLib.FileError error code. Exc: {e}") else: try: @@ -374,9 +522,8 @@ def remove_gtk_global_override(toast_overlay, settings, gtk_ver): global_keyfile.set_string_list( "Context", "filesystems", filesys_list) - global_save_keyfile( - toast_overlay, settings, global_keyfile, filename, gtk_ver - ) + __global_save_keyfile(global_keyfile, filename, + settings, gtk_ver, toast_overlay) logging.debug("remove override: Value removed.") else: set_theming() diff --git a/gradience/backend/globals.py b/gradience/backend/globals.py new file mode 100644 index 00000000..1f615b34 --- /dev/null +++ b/gradience/backend/globals.py @@ -0,0 +1,58 @@ +# globals.py +# +# Change the look of Adwaita, with ease +# Copyright (C) 2022 Gradience Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os + +from gi.repository import Xdp + + +presets_dir = os.path.join( + os.environ.get("XDG_CONFIG_HOME", os.environ["HOME"] + "/.config"), + "presets" +) + +preset_repos = { + "Official": "https://github.com/GradienceTeam/Community/raw/next/official.json", + "Curated": "https://github.com/GradienceTeam/Community/raw/next/curated.json" +} + +def get_gtk_theme_dir(app_type): + if app_type == "gtk4": + theme_dir = os.path.join( + os.environ.get("XDG_CONFIG_HOME", + os.environ["HOME"] + "/.config"), + "gtk-4.0" + ) + elif app_type == "gtk3": + theme_dir = os.path.join( + os.environ.get("XDG_CONFIG_HOME", + os.environ["HOME"] + "/.config"), + "gtk-3.0" + ) + + return theme_dir + +def is_sandboxed(): + portal = Xdp.Portal() + + is_sandboxed = self.portal.running_under_sandbox() + + return is_sandboxed + +def get_available_sassc(): + pass diff --git a/gradience/backend/logger.py b/gradience/backend/logger.py index efcc7ed6..f25ae378 100644 --- a/gradience/backend/logger.py +++ b/gradience/backend/logger.py @@ -25,9 +25,13 @@ class Logger(logging.getLoggerClass()): """ This is a wrapper of `logging` module. It provides custom formatting for log messages. + + Attributes: + logger_name (str): Custom name of the logger. + formatter (dict): Custom formatter for the logger. """ log_colors = { - "debug": 37, + "debug": 32, "info": 36, "warning": 33, "error": 31, @@ -35,21 +39,31 @@ class Logger(logging.getLoggerClass()): } log_format = { - 'fmt': '\033[1m[%(levelname)s]\033[0m [%(name)s] %(message)s' + 'fmt': '[%(name)s] %(message)s' } - def __set_color(self, level, message: str): + def __set_level_color(self, level, message: str): if message is not None and "\n" in message: message = message.replace("\n", "\n\t") + "\n" color_id = self.log_colors[level] - return "\033[%dm%s\033[0m" % (color_id, message) + return "\033[1;%dm%s:\033[0m %s" % (color_id, level.upper(), message) - def __init__(self, formatter=None): + def __init__(self, logger_name=None, formatter=None): + """ + The constructor for Logger class. + + When initializing this class, you should specify a logger name for debugging purposes, + even if you didn't wrote any debug messages in your code. + The logger name should usually be a name of your module's main class or module name. + """ if formatter is None: formatter = self.log_format formatter = logging.Formatter(**formatter) - self.root.name = "gradience" + if logger_name: + self.root.name = "Gradience.%s" % (logger_name) + else: + self.root.name = "Gradience" if build_type == "debug": self.root.setLevel(logging.DEBUG) @@ -62,19 +76,19 @@ class Logger(logging.getLoggerClass()): self.root.addHandler(handler) def debug(self, message, **kwargs): - self.root.debug(self.__set_color("debug", str(message)), ) + self.root.debug(self.__set_level_color("debug", str(message)), ) def info(self, message, **kwargs): - self.root.info(self.__set_color("info", str(message)), ) + self.root.info(self.__set_level_color("info", str(message)), ) def warning(self, message, **kwargs): - self.root.warning(self.__set_color("warning", str(message)),) + self.root.warning(self.__set_level_color("warning", str(message)),) def error(self, message, **kwargs): - self.root.error(self.__set_color("error", str(message)), ) + self.root.error(self.__set_level_color("error", str(message)), ) def critical(self, message, **kwargs): - self.root.critical(self.__set_color("critical", str(message)), ) + self.root.critical(self.__set_level_color("critical", str(message)), ) def set_silent(self): self.root.handlers = [] diff --git a/gradience/backend/meson.build b/gradience/backend/meson.build index 874407da..90f2734b 100644 --- a/gradience/backend/meson.build +++ b/gradience/backend/meson.build @@ -20,12 +20,14 @@ configure_file( ) subdir('models') +subdir('theming') subdir('utils') gradience_sources = [ '__init__.py', 'css_parser.py', 'flatpak_overrides.py', + 'globals.py', 'logger.py', 'preset_downloader.py' ] diff --git a/gradience/backend/models/preset.py b/gradience/backend/models/preset.py index 8d0cf6a9..95f8822b 100644 --- a/gradience/backend/models/preset.py +++ b/gradience/backend/models/preset.py @@ -19,23 +19,90 @@ import json import os -from gradience.frontend.settings_schema import settings_schema from gradience.backend.utils.common import to_slug_case +from gradience.backend.globals import presets_dir from gradience.backend.logger import Logger logging = Logger() -presets_dir = os.path.join( - os.environ.get("XDG_CONFIG_HOME", os.environ["HOME"] + "/.config"), - "presets", -) +# Adwaita default colors palette +adw_palette = { + "blue_": { + "1": "#99c1f1", + "2": "#62a0ea", + "3": "#3584e4", + "4": "#1c71d8", + "5": "#1a5fb4", + }, + "green_": { + "1": "#8ff0a4", + "2": "#57e389", + "3": "#33d17a", + "4": "#2ec27e", + "5": "#26a269", + }, + "yellow_": { + "1": "#f9f06b", + "2": "#f8e45c", + "3": "#f6d32d", + "4": "#f5c211", + "5": "#e5a50a", + }, + "orange_": { + "1": "#ffbe6f", + "2": "#ffa348", + "3": "#ff7800", + "4": "#e66100", + "5": "#c64600", + }, + "red_": { + "1": "#f66151", + "2": "#ed333b", + "3": "#e01b24", + "4": "#c01c28", + "5": "#a51d2d", + }, + "purple_": { + "1": "#dc8add", + "2": "#c061cb", + "3": "#9141ac", + "4": "#813d9c", + "5": "#613583", + }, + "brown_": { + "1": "#cdab8f", + "2": "#b5835a", + "3": "#986a44", + "4": "#865e3c", + "5": "#63452c", + }, + "light_": { + "1": "#ffffff", + "2": "#f6f5f4", + "3": "#deddda", + "4": "#c0bfbc", + "5": "#9a9996", + }, + "dark_": { + "1": "#77767b", + "2": "#5e5c64", + "3": "#3d3846", + "4": "#241f31", + "5": "#000000", + } +} + +custom_css_app_types = [ + "gtk4", + "gtk3" +] class Preset: variables = {} - palette = {} + palette = adw_palette custom_css = { "gtk4": "", "gtk3": "", @@ -43,33 +110,62 @@ class Preset: plugins = {} display_name = "New Preset" preset_path = "new_preset" + plugins_list = {} badges = {} - def __init__(self, preset_path=None, text=None, preset=None): - if preset_path: - self.load_preset(preset_path=preset_path) - elif text: # load from resource - self.load_preset(text=text) - elif preset: # css or dict - self.load_preset(preset=preset) - else: - raise Exception("Failed to create a new Preset object: Preset created without content") + def __init__(self): + pass + + def new(self, variables: dict, display_name=None, palette=None, custom_css=None, badges=None): + self.variables = variables + + if display_name: + self.display_name = display_name + if palette: + self.palette = palette + if custom_css: + self.custom_css = custom_css + if badges: + self.badges = badges + + def new_from_path(self, preset_path: str): + self.preset_path = preset_path - def load_preset(self, preset_path=None, text=None, preset=None): try: - if not preset: - if text: - preset_text = text - elif preset_path: - self.preset_path = preset_path - with open(self.preset_path, "r", encoding="utf-8") as file: - preset_text = file.read() - file.close() - else: - raise Exception("load_preset must be called with a path, text, or preset") + with open(self.preset_path, "r", encoding="utf-8") as file: + preset_text = file.read() + file.close() + except OSError as e: + logging.error(f"Failed to read contents of a preset in location: {self.preset_path}. Exc: {e}") - preset = json.loads(preset_text) + try: + preset = json.loads(preset_text) + except json.JSONDecodeError as e: + logging.error(f"Error while decoding JSON data. Exc: {e}") + self.__load_values(preset) + + return self + + def new_from_resource(self, text: str): + preset_text = text + + try: + preset = json.loads(preset_text) + except json.JSONDecodeError as e: + logging.error(f"Error while decoding JSON data. Exc: {e}") + + self.__load_values(preset) + + return self + + def new_from_dict(self, preset: dict): + self.__load_values(preset) + + return self + + def __load_values(self, preset): + try: self.display_name = preset["name"] self.variables = preset["variables"] self.palette = preset["palette"] @@ -82,31 +178,40 @@ class Preset: if "custom_css" in preset: self.custom_css = preset["custom_css"] else: - for app_type in settings_schema["custom_css_app_types"]: + for app_type in custom_css_app_types: self.custom_css[app_type] = "" except Exception as e: - if self.preset_path: - logging.error(f"Failed to load preset {self.preset_path}. Exc: {e}") - else: - logging.error(f"Failed to load preset with unknown path. Exc: {e}") + logging.error(f"Failed to create a new preset object. Exc: {e}") # Rename an existing preset - def rename_preset(self, name): + def rename(self, name): self.display_name = name old_path = self.preset_path self.preset_path = os.path.join( os.path.dirname(self.preset_path), to_slug_case(name) + ".json") - self.save_preset(to=self.preset_path) + self.save_to_file(to=self.preset_path) os.remove(old_path) + def get_preset_json(self, indent=None): + preset_dict = { + "name": self.display_name, + "variables": self.variables, + "palette": self.palette, + "custom_css": self.custom_css, + "plugins": self.plugins_list + } + json_output = json.dumps(preset_dict, indent=indent) + + return json_output + # Save a new user preset (or overwrite one) - def save_preset(self, name=None, plugins_list=None, to=None): + def save_to_file(self, name=None, plugins_list=None, to=None): self.display_name = name if name else self.display_name if to is None: - filename = to_slug_case(name) if name else "new_preset" + filename = to_slug_case(name) if name else to_slug_case(self.display_name) self.preset_path = os.path.join( presets_dir, "user", filename + ".json") else: @@ -118,28 +223,29 @@ class Preset: "user", ) ): - os.makedirs( - os.path.join( - presets_dir, - "user", + try: + os.makedirs( + os.path.join( + presets_dir, + "user", + ) ) - ) + except OSError as e: + logging.error(f"Failed to create a new preset directory. Exc: {e}") + raise - if plugins_list is None: - plugins_list = {} - else: + if plugins_list: plugins_list = plugins_list.save() - with open(self.preset_path, "w", encoding="utf-8") as file: - object_to_write = { - "name": self.display_name, - "variables": self.variables, - "palette": self.palette, - "custom_css": self.custom_css, - "plugins": plugins_list, - } - file.write(json.dumps(object_to_write, indent=4)) - file.close() + try: + with open(self.preset_path, "w", encoding="utf-8") as file: + content = self.get_preset_json(indent=4) + file.write(content) + file.close() + except OSError as e: + logging.error(f"Failed to save preset as a file. Exc: {e}") + raise + # TODO: Add validation def validate(self): return True diff --git a/gradience/backend/models/repo.py b/gradience/backend/models/repo.py index b6d9bf4b..1ae42a8c 100644 --- a/gradience/backend/models/repo.py +++ b/gradience/backend/models/repo.py @@ -19,7 +19,8 @@ import os from gradience.backend.utils.common import to_slug_case -from gradience.backend.models.preset import Preset, presets_dir +from gradience.backend.globals import presets_dir +from gradience.backend.models.preset import Preset class Repo: @@ -34,5 +35,6 @@ class Repo: presets = {} for preset in os.listdir(self.path): if preset.endswith(".json"): - presets[preset[:-5]] = Preset(os.path.join(self.path, preset)) + preset_path = os.path.join(self.path, preset) + presets[preset[:-5]] = Preset().new_from_path(preset_path) return presets diff --git a/gradience/backend/preset_downloader.py b/gradience/backend/preset_downloader.py index 4f6329c1..c5c2557a 100644 --- a/gradience/backend/preset_downloader.py +++ b/gradience/backend/preset_downloader.py @@ -21,7 +21,7 @@ import json from gi.repository import GLib, Soup -from gradience.backend.models.preset import presets_dir +from gradience.backend.globals import presets_dir from gradience.backend.utils.common import to_slug_case from gradience.backend.logger import Logger @@ -29,74 +29,77 @@ from gradience.backend.logger import Logger logging = Logger() -# Open Soup3 session -session = Soup.Session() +class PresetDownloader: + def __init__(self): + # Open Soup3 session + self.session = Soup.Session() -def fetch_presets(repo) -> [dict, list]: - try: - request = Soup.Message.new("GET", repo) - body = session.send_and_read(request, None) - except GLib.GError as e: # offline - if e.code == 1: - logging.error(f"Failed to establish a new connection. Exc: {e}") - return False, False - else: - logging.error(f"Unhandled Libsoup3 GLib.GError error code {e.code}. Exc: {e}") - return False, False - try: - raw = json.loads(body.get_data()) - except json.JSONDecodeError as e: - logging.error(f"Error with decoding JSON data. Exc: {e}") - return False, False + def fetch_presets(self, repo) -> [dict, list]: + try: + request = Soup.Message.new("GET", repo) + body = self.session.send_and_read(request, None) + except GLib.GError as e: + if e.code == 1: # offline + logging.error(f"Failed to establish a new connection. Exc: {e}") + raise + else: + logging.error(f"Unhandled Libsoup3 GLib.GError error code {e.code}. Exc: {e}") + raise + try: + raw = json.loads(body.get_data()) + except json.JSONDecodeError as e: + logging.error(f"Error while decoding JSON data. Exc: {e}") + raise - preset_dict = {} - url_list = [] + preset_dict = {} + url_list = [] - for data in raw.items(): - data = list(data) - data.insert(0, to_slug_case(data[0])) + for data in raw.items(): + data = list(data) + data.insert(0, to_slug_case(data[0])) - url = data[2] - data.pop(2) # Remove preset URL from list + url = data[2] + data.pop(2) # Remove preset URL from list - to_dict = iter(data) - # Convert list back to dict - preset_dict.update(dict(zip(to_dict, to_dict))) + to_dict = iter(data) + # Convert list back to dict + preset_dict.update(dict(zip(to_dict, to_dict))) - url_list.append(url) + url_list.append(url) - return preset_dict, url_list + return preset_dict, url_list -def download_preset(name, repo_name, repo) -> None: - try: - request = Soup.Message.new("GET", repo) - body = session.send_and_read(request, None) - except GLib.GError as e: # offline - if e.code == 1: - logging.error(f"Failed to establish a new connection. Exc: {e}") - return False, False - else: - logging.error(f"Unhandled Libsoup3 GLib.GError error code {e.code}. Exc: {e}") - return False, False - try: - raw = json.loads(body.get_data()) - except json.JSONDecodeError as e: - logging.error(f"Error with decoding JSON data. Exc: {e}") - return False, False + def download_preset(self, name, repo_name, repo) -> None: + try: + request = Soup.Message.new("GET", repo) + body = self.session.send_and_read(request, None) + except GLib.GError as e: + if e.code == 1: # offline + logging.error(f"Failed to establish a new connection. Exc: {e}") + raise + else: + logging.error(f"Unhandled Libsoup3 GLib.GError error code {e.code}. Exc: {e}") + raise + try: + raw = json.loads(body.get_data()) + except json.JSONDecodeError as e: + logging.error(f"Error while decoding JSON data. Exc: {e}") + raise - data = json.dumps(raw, indent=4) + data = json.dumps(raw, indent=4) - try: - with open( - os.path.join( - presets_dir, - repo_name, - to_slug_case(name) + ".json", - ), - "w", - encoding="utf-8", - ) as f: - f.write(data) - f.close() - except OSError as e: - logging.error(f"Failed to write data to a file. Exc: {e}") + try: + with open( + os.path.join( + presets_dir, + repo_name, + to_slug_case(name) + ".json", + ), + "w", + encoding="utf-8", + ) as f: + f.write(data) + f.close() + except OSError as e: + logging.error(f"Failed to write data to a file. Exc: {e}") + raise diff --git a/gradience/backend/theming/__init__.py b/gradience/backend/theming/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/gradience/backend/theming/meson.build b/gradience/backend/theming/meson.build new file mode 100644 index 00000000..8b40abbc --- /dev/null +++ b/gradience/backend/theming/meson.build @@ -0,0 +1,8 @@ +themingdir = 'gradience/backend/theming' + +gradience_sources = [ + '__init__.py', + 'monet.py', + 'preset_utils.py' +] +PY_INSTALLDIR.install_sources(gradience_sources, subdir: themingdir) diff --git a/gradience/backend/theming/monet.py b/gradience/backend/theming/monet.py new file mode 100644 index 00000000..057f4dd8 --- /dev/null +++ b/gradience/backend/theming/monet.py @@ -0,0 +1,63 @@ +# monet.py +# +# Change the look of Adwaita, with ease +# Copyright (C) 2022 Gradience Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os + +from svglib.svglib import svg2rlg +from reportlab.graphics import renderPM +import material_color_utilities_python as monet + +from gradience.backend.models.preset import Preset + +from gradience.backend.logger import Logger + +logging = Logger() + + +class Monet: + def __init__(self): + self.palette = None + + def generate_from_image(self, image_path: str) -> dict: + if image_path.endswith(".svg"): + drawing = svg2rlg(image_path) + image_path = os.path.join( + os.environ.get("XDG_RUNTIME_DIR"), "gradience_bg.png" + ) + renderPM.drawToFile(drawing, image_path, fmt="PNG") + + if image_path.endswith(".xml"): + logging.error(f"XML files are unsupported by Gradience's Monet implementation.") + return False + + try: + monet_img = monet.Image.open(image_path) + except Exception as e: + logging.error(f"An error occurred while generating a Monet palette. Exc: {e}") + return False + else: + basewidth = 64 + wpercent = basewidth / float(monet_img.size[0]) + hsize = int((float(monet_img.size[1]) * float(wpercent))) + monet_img = monet_img.resize( + (basewidth, hsize), monet.Image.Resampling.LANCZOS + ) + + self.palette = monet.themeFromImage(monet_img) + + return self.palette diff --git a/gradience/backend/theming/preset_utils.py b/gradience/backend/theming/preset_utils.py new file mode 100644 index 00000000..aeb01541 --- /dev/null +++ b/gradience/backend/theming/preset_utils.py @@ -0,0 +1,366 @@ +# preset_utils.py +# +# Change the look of Adwaita, with ease +# Copyright (C) 2022 Gradience Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os +import json +from pathlib import Path + +from gi.repository import GLib, Gio + +from gradience.backend.theming.monet import Monet +from gradience.backend.models.preset import Preset +from gradience.backend.utils.colors import rgba_from_argb + +from gradience.backend.globals import presets_dir, get_gtk_theme_dir + +from gradience.backend.logger import Logger + +logging = Logger() + + +class PresetUtils: + def __init__(self): + self.preset = Preset() + + def generate_gtk_css(self, app_type: str, preset: Preset) -> str: + variables = preset.variables + palette = preset.palette + custom_css = preset.custom_css + + final_css = "" + + for key in variables.keys(): + final_css += f"@define-color {key} {variables[key]};\n" + + for prefix_key in palette.keys(): + for key in palette[prefix_key].keys(): + final_css += f"@define-color {prefix_key + key} {palette[prefix_key][key]};\n" + + final_css += custom_css.get(app_type, "") + + return final_css + + def new_preset_from_monet(self, name=None, monet_palette=None, props=None, obj_only=False) -> Preset or None: + if props: + tone = props[0] + theme = props[1] + else: + raise Exception("Properties 'tone' and/or 'theme' missing") + + if not monet_palette: + raise Exception("Property 'monet_palette' missing") + + if theme == "dark": + dark_theme = monet_palette["schemes"]["dark"] + variable = { + "accent_color": rgba_from_argb(dark_theme.primary), + "accent_bg_color": rgba_from_argb(dark_theme.primaryContainer), + "accent_fg_color": rgba_from_argb(dark_theme.onPrimaryContainer), + "destructive_color": rgba_from_argb(dark_theme.error), + "destructive_bg_color": rgba_from_argb(dark_theme.errorContainer), + "destructive_fg_color": rgba_from_argb( + dark_theme.onErrorContainer + ), + "success_color": rgba_from_argb(dark_theme.tertiary), + "success_bg_color": rgba_from_argb(dark_theme.onTertiary), + "success_fg_color": rgba_from_argb(dark_theme.onTertiaryContainer), + "warning_color": rgba_from_argb(dark_theme.secondary), + "warning_bg_color": rgba_from_argb(dark_theme.onSecondary), + "warning_fg_color": rgba_from_argb(dark_theme.primary, "0.8"), + "error_color": rgba_from_argb(dark_theme.error), + "error_bg_color": rgba_from_argb(dark_theme.errorContainer), + "error_fg_color": rgba_from_argb(dark_theme.onError), + "window_bg_color": rgba_from_argb(dark_theme.surface), + "window_fg_color": rgba_from_argb(dark_theme.onSurface), + "view_bg_color": rgba_from_argb(dark_theme.surface), + "view_fg_color": rgba_from_argb(dark_theme.onSurface), + "headerbar_bg_color": rgba_from_argb(dark_theme.surface), + "headerbar_fg_color": rgba_from_argb(dark_theme.onSurface), + "headerbar_border_color": rgba_from_argb( + dark_theme.primary, "0.8" + ), + "headerbar_backdrop_color": "@headerbar_bg_color", + "headerbar_shade_color": rgba_from_argb(dark_theme.shadow), + "card_bg_color": rgba_from_argb(dark_theme.primary, "0.05"), + "card_fg_color": rgba_from_argb(dark_theme.onSecondaryContainer), + "card_shade_color": rgba_from_argb(dark_theme.shadow), + "dialog_bg_color": rgba_from_argb(dark_theme.secondaryContainer), + "dialog_fg_color": rgba_from_argb(dark_theme.onSecondaryContainer), + "popover_bg_color": rgba_from_argb(dark_theme.secondaryContainer), + "popover_fg_color": rgba_from_argb( + dark_theme.onSecondaryContainer + ), + "shade_color": rgba_from_argb(dark_theme.shadow), + "scrollbar_outline_color": rgba_from_argb(dark_theme.outline), + } + elif theme == "light": + light_theme = monet_palette["schemes"]["light"] + variable = { + "accent_color": rgba_from_argb(light_theme.primary), + "accent_bg_color": rgba_from_argb(light_theme.primary), + "accent_fg_color": rgba_from_argb(light_theme.onPrimary), + "destructive_color": rgba_from_argb(light_theme.error), + "destructive_bg_color": rgba_from_argb(light_theme.errorContainer), + "destructive_fg_color": rgba_from_argb( + light_theme.onErrorContainer + ), + "success_color": rgba_from_argb(light_theme.tertiary), + "success_bg_color": rgba_from_argb(light_theme.tertiaryContainer), + "success_fg_color": rgba_from_argb( + light_theme.onTertiaryContainer + ), + "warning_color": rgba_from_argb(light_theme.secondary), + "warning_bg_color": rgba_from_argb(light_theme.secondaryContainer), + "warning_fg_color": rgba_from_argb( + light_theme.onSecondaryContainer + ), + "error_color": rgba_from_argb(light_theme.error), + "error_bg_color": rgba_from_argb(light_theme.errorContainer), + "error_fg_color": rgba_from_argb(light_theme.onError), + "window_bg_color": rgba_from_argb(light_theme.secondaryContainer), + "window_fg_color": rgba_from_argb(light_theme.onSurface), + "view_bg_color": rgba_from_argb(light_theme.secondaryContainer), + "view_fg_color": rgba_from_argb(light_theme.onSurface), + "headerbar_bg_color": rgba_from_argb( + light_theme.secondaryContainer + ), + "headerbar_fg_color": rgba_from_argb(light_theme.onSurface), + "headerbar_border_color": rgba_from_argb( + light_theme.primary, "0.8" + ), + "headerbar_backdrop_color": "@headerbar_bg_color", + "headerbar_shade_color": rgba_from_argb( + light_theme.secondaryContainer + ), + "card_bg_color": rgba_from_argb(light_theme.primary, "0.05"), + "card_fg_color": rgba_from_argb(light_theme.onSecondaryContainer), + "card_shade_color": rgba_from_argb(light_theme.shadow), + "dialog_bg_color": rgba_from_argb(light_theme.secondaryContainer), + "dialog_fg_color": rgba_from_argb( + light_theme.onSecondaryContainer + ), + "popover_bg_color": rgba_from_argb(light_theme.secondaryContainer), + "popover_fg_color": rgba_from_argb( + light_theme.onSecondaryContainer + ), + "shade_color": rgba_from_argb(light_theme.shadow), + "scrollbar_outline_color": rgba_from_argb(light_theme.outline), + } + + if obj_only == False and not name: + raise Exception("You either need to set 'obj_only' property to True, or add value to 'name' property") + + if obj_only: + if name: + print("with name, obj_only") + self.preset.new(variables=variable, display_name=name) + else: + print("no name, obj_only") + self.preset.new(variables=variable) + return self.preset + + if obj_only == False: + print("no obj_only, name") + self.preset.new(variables=variable, display_name=name) + + try: + self.preset.save_to_file() + except Exception as e: + # TODO: Move exception handling to model/preset module + logging.error(f"Unexpected file error while trying to generate preset from Monet palette. Exc: {e}") + raise + + def get_presets_list(self, json_output=False) -> dict: + presets_list = {} + + for repo in Path(presets_dir).iterdir(): + logging.debug(f"presets_dir.iterdir: {repo}") + if repo.is_dir(): + for file_name in repo.iterdir(): + file_name = str(file_name) + if file_name.endswith(".json"): + try: + with open( + os.path.join(presets_dir, file_name), + "r", + encoding="utf-8", + ) as file: + preset_text = file.read() + file.close() + preset = json.loads(preset_text) + if preset.get("variables") is None: + raise KeyError("'variables' section missing in loaded preset file") + if preset.get("palette") is None: + raise KeyError("'palette' section missing in loaded preset file") + presets_list[file_name] = preset[ + "name" + ] + except (OSError, KeyError) as e: + logging.error(f"Failed to load an preset information. Exc: {e}") + raise + elif repo.is_file(): + # this exists to keep compatibility with old presets + if repo.name.endswith(".json"): + logging.warning("Legacy preset found. Moving to new structure.") + if not os.path.isdir(os.path.join(presets_dir, "user")): + os.mkdir(os.path.join(presets_dir, "user")) + + os.rename(repo, os.path.join( + presets_dir, "user", repo.name)) + + try: + with open( + os.path.join(presets_dir, "user", repo), + "r", + encoding="utf-8", + ) as file: + preset_text = file.read() + preset = json.loads(preset_text) + if preset.get("variables") is None: + raise KeyError("'variables' section missing in loaded preset file") + if preset.get("palette") is None: + raise KeyError("'palette' section missing in loaded preset file") + presets_list["user"][file_name] = preset[ + "name" + ] + except (OSError, KeyError) as e: + logging.error(f"Failed to load an preset information. Exc: {e}") + raise + if json_output: + json_output = json.dumps(presets_list) + return json_output + return presets_list + + def apply_preset(self, app_type: str, preset: Preset) -> None: + if app_type == "gtk4": + theme_dir = get_gtk_theme_dir(app_type) + + if not os.path.exists(theme_dir): + os.makedirs(theme_dir) + + gtk4_css = self.generate_gtk_css("gtk4", preset) + contents = "" + + try: + with open( + os.path.join(theme_dir, "gtk.css"), "r", encoding="utf-8" + ) as file: + contents = file.read() + except FileNotFoundError: # first run + pass + else: + with open( + os.path.join(theme_dir, "gtk.css.bak"), "w", encoding="utf-8" + ) as file: + file.write(contents) + finally: + with open( + os.path.join(theme_dir, "gtk.css"), "w", encoding="utf-8" + ) as file: + file.write(gtk4_css) + elif app_type == "gtk3": + theme_dir = get_gtk_theme_dir(app_type) + + if not os.path.exists(theme_dir): + os.makedirs(theme_dir) + + gtk3_css = self.generate_gtk_css("gtk3", preset) + contents = "" + + try: + with open( + os.path.join(theme_dir, "gtk.css"), "r", encoding="utf-8" + ) as file: + contents = file.read() + except FileNotFoundError: # first run + pass + else: + with open( + os.path.join(theme_dir, "gtk.css.bak"), "w", encoding="utf-8" + ) as file: + file.write(contents) + finally: + with open( + os.path.join(theme_dir, "gtk.css"), "w", encoding="utf-8" + ) as file: + file.write(gtk3_css) + + def restore_gtk4_preset(self) -> None: + try: + with open( + os.path.join( + os.environ.get( + "XDG_CONFIG_HOME", os.environ["HOME"] + + "/.config" + ), + "gtk-4.0/gtk.css.bak", + ), + "r", + encoding="utf-8", + ) as backup: + contents = backup.read() + backup.close() + + with open( + os.path.join( + os.environ.get( + "XDG_CONFIG_HOME", os.environ["HOME"] + + "/.config" + ), + "gtk-4.0/gtk.css", + ), + "w", + encoding="utf-8", + ) as gtk4css: + gtk4css.write(contents) + gtk4css.close() + except OSError as e: + logging.error(f"Unable to restore Gtk4 backup. Exc: {e}") + raise + + def reset_preset(self, app_type: str) -> None: + if app_type == "gtk4": + file = Gio.File.new_for_path( + os.path.join( + os.environ.get( + "XDG_CONFIG_HOME", os.environ["HOME"] + "/.config" + ), + "gtk-4.0/gtk.css", + ) + ) + + try: + file.delete() + except GLib.GError as e: + logging.error(f"Unable to delete current preset. Exc: {e}") + raise + elif app_type == "gtk3": + file = Gio.File.new_for_path( + os.path.join( + os.environ.get( + "XDG_CONFIG_HOME", os.environ["HOME"] + "/.config" + ), + "gtk-3.0/gtk.css", + ) + ) + + try: + file.delete() + except GLib.GError as e: + logging.error(f"Unable to delete current preset. Exc: {e}") + raise diff --git a/gradience/backend/utils/colors.py b/gradience/backend/utils/colors.py new file mode 100644 index 00000000..4163e83f --- /dev/null +++ b/gradience/backend/utils/colors.py @@ -0,0 +1,31 @@ +# colors.py +# +# Change the look of Adwaita, with ease +# Copyright (C) 2022 Gradience Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import material_color_utilities_python as monet + + +def rgba_from_argb(argb, alpha=None) -> str: + base = "rgba({}, {}, {}, {})" + + red = monet.redFromArgb(argb) + green = monet.greenFromArgb(argb) + blue = monet.blueFromArgb(argb) + if not alpha: + alpha = monet.alphaFromArgb(argb) + + return base.format(red, green, blue, alpha) diff --git a/gradience/backend/utils/common.py b/gradience/backend/utils/common.py index 611ce3ed..34b946ab 100644 --- a/gradience/backend/utils/common.py +++ b/gradience/backend/utils/common.py @@ -23,7 +23,7 @@ import subprocess from anyascii import anyascii -def to_slug_case(non_slug): +def to_slug_case(non_slug) -> str: return re.sub(r"[^0-9a-z]+", "-", anyascii(non_slug).lower()).strip("-") def run_command(command, *args, **kwargs): diff --git a/gradience/backend/utils/meson.build b/gradience/backend/utils/meson.build index 588667a5..30855aa5 100644 --- a/gradience/backend/utils/meson.build +++ b/gradience/backend/utils/meson.build @@ -2,6 +2,7 @@ utilsdir = 'gradience/backend/utils' gradience_sources = [ '__init__.py', + 'colors.py', 'common.py' ] PY_INSTALLDIR.install_sources(gradience_sources, subdir: utilsdir) diff --git a/gradience/frontend/cli/__init__.py b/gradience/frontend/cli/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/gradience/frontend/cli/cli.in b/gradience/frontend/cli/cli.in new file mode 100755 index 00000000..f4171847 --- /dev/null +++ b/gradience/frontend/cli/cli.in @@ -0,0 +1,416 @@ +#!/usr/bin/env python3 + +# cli.in +# +# Change the look of Adwaita, with ease +# Copyright (C) 2022 Gradience Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os +import sys +import json +import shutil +import signal +import argparse +import warnings + +version = "@VERSION@" +is_local = @local_build@ + +if is_local: + # In the local use case, use gradience module from the sourcetree + sys.path.insert(1, '@PYTHON@') + + # In the local use case the installed schemas go in /data + os.environ["XDG_DATA_DIRS"] = '@SCHEMAS_DIR@:' + os.environ.get("XDG_DATA_DIRS", "") + +signal.signal(signal.SIGINT, signal.SIG_DFL) + +warnings.filterwarnings("ignore") # suppress GTK warnings + +from gi.repository import GLib, Gio + +from gradience.backend.utils.common import to_slug_case +from gradience.backend.globals import preset_repos, presets_dir + +from gradience.backend.theming.monet import Monet +from gradience.backend.models.preset import Preset +from gradience.backend.theming.preset_utils import PresetUtils +from gradience.backend.preset_downloader import PresetDownloader +from gradience.backend.flatpak_overrides import list_file_access, allow_file_access, disallow_file_access, create_gtk_user_override, remove_gtk_user_override + +from gradience.backend.logger import Logger + +logging = Logger() + + +class CLI: + settings = Gio.Settings.new("@APP_ID@") + + def __init__(self): + self.parser = argparse.ArgumentParser(description="Gradience - Change the look of Adwaita, with ease") + self.parser.add_argument("-V", "--version", action="version", version=f"Gradience, version {version}") + #self.parser.add_argument("-j", "--json", action="store_true", help="print out a result of the command directly in JSON format") + #self.parser.add_argument('-J', '--pretty-json', dest='pretty_json', action='store_true', help='pretty-print JSON output') + + subparsers = self.parser.add_subparsers(dest="command") + + #info_parser = subparsers.add_parser("info", help="show information about Gradience") + + presets_parser = subparsers.add_parser("presets", help="list installed presets") + #presets_parser.add_argument("-r", "--remove-preset", metavar="PRESET_NAME", help="remove a preset from the list") + presets_parser.add_argument("-j", "--json", action="store_true", help="print out a result of this command directly in JSON format") + + favorites_parser = subparsers.add_parser("favorites", help="list favorite presets") + favorites_parser.add_argument("-a", "--add-preset", metavar="PRESET_NAME", help="add a preset to favorites") + favorites_parser.add_argument("-r", "--remove-preset", metavar="PRESET_NAME", help="remove a preset from favorites") + favorites_parser.add_argument("-j", "--json", action="store_true", help="print out a result of this command directly in JSON format") + + import_parser = subparsers.add_parser("import", help="import a preset") + import_parser.add_argument("-p", "--preset-path", help="absolute path to a preset file", required=True) + + apply_parser = subparsers.add_parser("apply", help="apply a preset") + apply_group = apply_parser.add_mutually_exclusive_group(required=True) + apply_group.add_argument("-n", "--preset-name", help="display name for a preset") + apply_group.add_argument("-p", "--preset-path", help="absolute path to a preset file") + apply_parser.add_argument("--gtk", choices=["gtk4", "gtk3", "both"], default="gtk4", help="types of applications you want to theme (default: gtk4)") + #apply_parser.add_argument("--flatpak", choices=["gtk4", "gtk3", "both"], help="types of Flatpak applications you want to theme (for GTK3 option, make sure you have adw-gtk3 installed as Flatpak)") + + #new_parser = subparsers.add_parser("new", help="create a new preset") + #new_parser.add_argument("-i", "--interactive", action="store_true", help="") + #new_parser.add_argument("-n", "--name", help="display name for a preset", required=True) + #new_parser.add_argument("--colors", help="", required=True) + #new_parser.add_argument("--palette", help="") + #new_parser.add_argument("--custom-css", help="") + #new_parser.add_argument("-j", "--json", action="store_true", help="print out a result of this command directly in JSON format") + + download_parser = subparsers.add_parser("download", help="download preset from a preset repository") + #new_parser.add_argument("-i", "--interactive", action="store_true", help="") + download_parser.add_argument("-n", "--preset-name", help="name of a preset you want to get", required=True) + #download_parser.add_argument("--custom-url", help="use custom repository's presets.json to download other presets") + + monet_parser = subparsers.add_parser("monet", help="generate Material You preset from an image") + monet_parser.add_argument("-n", "--preset-name", help="name for a generated preset", required=True) + monet_parser.add_argument("-p", "--image-path", help="abosulte path to image", required=True) + monet_parser.add_argument("--tone", default=20, help="a tone for colors (default: 20)") + monet_parser.add_argument("--theme", choices=["light", "dark"], default="light", help="choose whatever it should be a light or dark theme (default: light)") + monet_parser.add_argument("-j", "--json", action="store_true", help="print out a result of this command directly in JSON format") + + access_parser = subparsers.add_parser("access-file", help="allow or disallow Gradience to access a certain file or directory") + access_parser.add_argument("-l", "--list", action="store_true", help="list allowed directories and files") + access_group = access_parser.add_mutually_exclusive_group(required=False) + access_group.add_argument("-a", "--allow", metavar="PATH", help="allow Gradience access to this file or directory") + access_group.add_argument("-d", "--disallow", metavar="PATH", help="disallow Gradience access to this file or directory") + + overrides_parser = subparsers.add_parser("flatpak-overrides", help="enable or disable Flatpak theming") + overrides_group = overrides_parser.add_mutually_exclusive_group(required=True) + overrides_group.add_argument("-e", "--enable-theming", choices=["gtk4", "gtk3", "both"], help="enable overrides for Flatpak theming") + overrides_group.add_argument("-d", "--disable-theming", choices=["gtk4", "gtk3", "both"], help="disable overrides for Flatpak theming") + + self.__parse_args() + + def __print_json(self, data, pretty=False): + if pretty: + print(json.dumps(data, indent=4)) + else: + print(json.dumps(data)) + + def __parse_args(self): + args = self.parser.parse_args() + + if not args.command: + print(self.parser.format_help()) + + if args.command == "presets": + self.list_presets(args) + + elif args.command == "favorites": + self.favorite_presets(args) + + elif args.command == "import": + self.import_preset(args) + + elif args.command == "apply": + self.apply_preset(args) + + elif args.command == "new": + self.new_preset(args) + + elif args.command == "download": + self.download_preset(args) + + elif args.command == "monet": + self.generate_monet(args) + + elif args.command == "access-file": + self.access_file(args) + + elif args.command == "flatpak-overrides": + self.flatpak_theming(args) + + def list_presets(self, args): + #_remove_preset = args.remove_preset + _json = args.json + + if _json: + presets_list = PresetUtils().get_presets_list(json_output=True) + print(presets_list) + exit(0) + + presets_list = PresetUtils().get_presets_list() + + # TODO: Modify this output to look more like a table (maybe use ncurses?) + print("\033[1;37mPreset name\033[0m | \033[1;37mPreset path\033[0m") + for key in presets_list: + print(f"{presets_list[key]} -> {key}") + + def favorite_presets(self, args): + _add_preset = args.add_preset + _remove_preset = args.remove_preset + _json = args.json + + favorite = set(self.settings.get_value("favourite")) + presets_list = PresetUtils().get_presets_list() + presets_name = list(presets_list.values()) + + if _json and not _add_preset and not _remove_preset: + favorites_json = {"favorites": list(favorite), "amount": len(favorite)} + json_output = json.dumps(favorites_json) + print(json_output) + exit(0) + elif _json and _add_preset or _json and _remove_preset: + logging.error("JSON output option isn't available for --add-preset and --remove-preset options.") + exit(1) + + if _add_preset: + if _add_preset in presets_name: + favorite.add(_add_preset) + self.settings.set_value("favourite", GLib.Variant("as", favorite)) + logging.info(f"Preset {_add_preset} has been added to favorites.") + exit(0) + else: + logging.error(f"Preset named {_add_preset} isn't installed in Gradience. " + "Check if you typed the correct preset name, or try importing your preset using 'import' command.") + exit(1) + + if _remove_preset: + if _remove_preset in favorite: + favorite.remove(_remove_preset) + self.settings.set_value("favourite", GLib.Variant("as", favorite)) + logging.info(f"Preset {_add_preset} has been removed from favorites.") + exit(0) + else: + logging.error(f"Preset named {_remove_preset} doesn't exist in favorites list. " + "Check if you typed the correct preset name.") + exit(1) + + logging.info("Favorite presets list:") + for i, preset in enumerate(favorite): + print(preset) + + logging.info(f"Favorites amount: {len(favorite)}") + exit(0) + + def import_preset(self, args): + _preset_path = args.preset_path + + preset_file = GLib.path_get_basename(_preset_path) + logging.info(f"Importing preset: {preset_file.strip()}") + + # TODO: Check if preset is already imported + if _preset_path.endswith(".json"): + try: + shutil.copy( + _preset_path, + os.path.join( + presets_dir, + "user", + preset_file.strip() + ) + ) + except FileNotFoundError as e: + logging.error(f"Preset could not be imported. Exc: {e}") + exit(1) + logging.info("Preset imported successfully.") + else: + logging.error("Unsupported file format, must be .json") + exit(1) + + def apply_preset(self, args): + _preset_name = args.preset_name + _preset_path = args.preset_path + _gtk = args.gtk + #_flatpak = args.flatpak + + presets_list = PresetUtils().get_presets_list() + presets_name = list(presets_list.values()) + + if _preset_name: + if _preset_name in presets_name: + for path, name in presets_list.items(): + if name == _preset_name: + preset = Preset().new_from_path(path) + elif _preset_path: + preset = Preset().new_from_path(_preset_path) + + if _gtk == "gtk4": + PresetUtils().apply_preset("gtk4", preset) + logging.info(f"Preset {preset.display_name} applied successfully for Gtk 4 applications.") + elif _gtk == "gtk3": + PresetUtils().apply_preset("gtk3", preset) + logging.info(f"Preset {preset.display_name} applied successfully for Gtk 3 applications.") + logging.info("In order for changes to take full effect, you need to log out.") + + def new_preset(self, args): + #_interactive = args.interactive + _name = args.name + _colors = args.colors + _palette = args.palette + _custom_css = args.custom_css + _json = args.json + + # TODO: Do the logic code for `new` command + + logging.error("This command isn't implemented yet") + exit(1) + + def download_preset(self, args): + #_interactive = args.interactive + _preset_name = args.preset_name + #_custom_url = args.custom_url + + repo_no = 1 + repos_amount = len(preset_repos.items()) + for repo_name, repo in preset_repos.items(): + try: + explore_presets, urls = PresetDownloader().fetch_presets(repo) + except (GLib.GError, json.JSONDecodeError) as e: + logging.error(f"An error occurred while fetching presets from remote repository. Exc: {e}") + exit(1) + else: + preset_no = 1 + presets_amount = len(explore_presets.items()) + for (preset, preset_name), preset_url in zip(explore_presets.items(), urls): + # TODO: Add handling of two or more presets with the same elements in name + if _preset_name.lower() in preset_name.lower(): + logging.info(f"Downloading preset: {preset_name}") + try: + PresetDownloader().download_preset(preset_name, to_slug_case(repo_name), preset_url) + except (GLib.GError, json.JSONDecodeError, OSError) as e: + logging.error(f"An error occurred while downloading a preset. Exc: {e}") + exit(1) + else: + logging.info("Preset downloaded successfully.") + exit(0) + else: + if repo_no == repos_amount and preset_no == presets_amount: + logging.error(f"No presets found with text: {_preset_name}") + exit(1) + preset_no += 1 + continue + repo_no += 1 + + # NOTE: Possible useful portals to use in future: org.freedesktop.portal.Documents (support missing in libportal, only D-Bus calls), org.freedesktop.portal.FileChooser + def generate_monet(self, args): + _preset_name = args.preset_name + _image_path = args.image_path + _tone = args.tone + _theme = args.theme + _json = args.json + + palette = Monet().generate_from_image(_image_path) + props = [_tone, _theme] + + if _json: + preset = PresetUtils().new_preset_from_monet(name=_preset_name, monet_palette=palette, + props=props, obj_only=True) + preset_json = preset.get_preset_json() + print(preset_json) + exit(0) + + PresetUtils().new_preset_from_monet(_preset_name, palette, props) + logging.info("In order for changes to take full effect, you need to log out.") + + # TODO: Add path and xdg-* values parsing + def access_file(self, args): + _list = args.list + _allow = args.allow + _disallow = args.disallow + + if _list: + try: + access_list = list_file_access() + except GLib.GError as e: + logging.error(f"An error occurred while accessing allowed files list. Exc: {e}") + exit(1) + else: + logging.info("Allowed files:") + if access_list: + for value in access_list: + print(value) + exit(0) + else: + print("0 paths found") + exit(0) + + if _allow: + try: + allow_file_access(_allow) + except GLib.GError as e: + logging.error(f"An error occurred while setting file access. Exc: {e}") + exit(1) + else: + logging.info(f"Path {_allow} added to access list") + exit(0) + + if _disallow: + try: + disallow_file_access(_disallow) + except GLib.GError as e: + logging.error(f"An error occurred while setting file access. Exc: {e}") + exit(1) + else: + logging.info(f"Path {_disallow} removed from access list") + exit(0) + + def flatpak_theming(self, args): + _enable_theming = args.enable_theming + _disable_theming = args.disable_theming + + if _enable_theming == "gtk4": + create_gtk_user_override(self.settings, "gtk4") + logging.info("Flatpak theming for Gtk 4 applications has been enabled.") + elif _enable_theming == "gtk3": + create_gtk_user_override(self.settings, "gtk3") + logging.info("Flatpak theming for Gtk 3 applications has been enabled.") + elif _enable_theming == "both": + create_gtk_user_override(self.settings, "gtk4") + create_gtk_user_override(self.settings, "gtk3") + logging.info("Flatpak theming for Gtk 4 and Gtk 3 applications has been enabled.") + + if _disable_theming == "gtk4": + remove_gtk_user_override(self.settings, "gtk4") + logging.info("Flatpak theming for Gtk 4 applications has been disabled.") + elif _disable_theming == "gtk3": + remove_gtk_user_override(self.settings, "gtk3") + logging.info("Flatpak theming for Gtk 3 applications has been disabled.") + elif _disable_theming == "both": + remove_gtk_user_override(self.settings, "gtk4") + remove_gtk_user_override(self.settings, "gtk3") + logging.info("Flatpak theming for Gtk 4 and Gtk 3 applications has been disabled.") + + +if __name__ == "__main__": + cli = CLI() diff --git a/gradience/frontend/cli/meson.build b/gradience/frontend/cli/meson.build new file mode 100644 index 00000000..84c61f35 --- /dev/null +++ b/gradience/frontend/cli/meson.build @@ -0,0 +1,22 @@ +clidir = 'gradience/frontend/cli' + +configure_file( + input: 'cli.in', + output: 'gradience-cli', + configuration: conf, + install: true, + install_dir: get_option('bindir') +) + +configure_file( + input: 'cli.in', + output: 'gradience-cli', + configuration: local_conf, + install: true, + install_dir: join_paths(meson.project_build_root(), 'gradience', 'frontend') +) + +gradience_sources = [ + '__init__.py' +] +PY_INSTALLDIR.install_sources(gradience_sources, subdir: clidir) diff --git a/gradience/frontend/main.py b/gradience/frontend/main.py index f07e3fa2..08efada0 100644 --- a/gradience/frontend/main.py +++ b/gradience/frontend/main.py @@ -25,8 +25,11 @@ from pathlib import Path from material_color_utilities_python import * from gi.repository import Gtk, Gdk, Gio, Adw, GLib, Xdp, XdpGtk4 +from gradience.backend.globals import presets_dir from gradience.backend.css_parser import parse_css -from gradience.backend.models.preset import Preset, presets_dir +from gradience.backend.models.preset import Preset +from gradience.backend.theming.preset_utils import PresetUtils +from gradience.backend.utils.colors import rgba_from_argb from gradience.backend.utils.common import to_slug_case from gradience.backend.constants import * @@ -288,7 +291,7 @@ class GradienceApplication(Adw.Application): "palette": palette, "custom_css": {"gtk4": custom_css}, } - self.preset = Preset(preset=preset) + self.preset = Preset().new_from_dict(preset) self.load_preset_variables_from_preset() except OSError: # fallback to adwaita logging.warning("Custom preset not found. Fallback to Adwaita") @@ -320,13 +323,13 @@ class GradienceApplication(Adw.Application): def load_preset_from_file(self, preset_path): logging.debug(f"load preset from file {preset_path}") - self.preset = Preset(preset_path=preset_path) + self.preset = Preset().new_from_path(preset_path) self.load_preset_variables_from_preset() def load_preset_from_resource(self, preset_path): preset_text = Gio.resources_lookup_data( preset_path, 0).get_data().decode() - self.preset = Preset(text=preset_text) + self.preset = Preset().new_from_resource(text=preset_text) self.load_preset_variables_from_preset() def load_preset_variables_from_preset(self, preset=None): @@ -374,131 +377,27 @@ class GradienceApplication(Adw.Application): self.reload_variables() - @staticmethod - def rgba_from_argb(argb, alpha=None) -> str: - base = "rgba({}, {}, {}, {})" - - red = redFromArgb(argb) - green = greenFromArgb(argb) - blue = blueFromArgb(argb) - if not alpha: - alpha = alphaFromArgb(argb) - - return base.format(red, green, blue, alpha) - - def update_theme_from_monet(self, theme, tone, monet_theme): - palettes = theme["palettes"] + def update_theme_from_monet(self, monet, tone, monet_theme): + palettes = monet["palettes"] monet_theme = monet_theme.get_string().lower() # dark / light palette = {} - i = 0 - for color in palettes.values(): - i += 1 + + for i, color in zip(range(1, 7), palettes.values()): palette[str(i)] = hexFromArgb(color.tone(int(tone.get_string()))) self.pref_palette_shades["monet"].update_shades(palette) + if monet_theme == "auto": if self.style_manager.get_dark(): monet_theme = "dark" else: monet_theme = "light" - if monet_theme == "dark": - dark_theme = theme["schemes"]["dark"] - variable = { - "accent_color": self.rgba_from_argb(dark_theme.primary), - "accent_bg_color": self.rgba_from_argb(dark_theme.primaryContainer), - "accent_fg_color": self.rgba_from_argb(dark_theme.onPrimaryContainer), - "destructive_color": self.rgba_from_argb(dark_theme.error), - "destructive_bg_color": self.rgba_from_argb(dark_theme.errorContainer), - "destructive_fg_color": self.rgba_from_argb( - dark_theme.onErrorContainer - ), - "success_color": self.rgba_from_argb(dark_theme.tertiary), - "success_bg_color": self.rgba_from_argb(dark_theme.onTertiary), - "success_fg_color": self.rgba_from_argb(dark_theme.onTertiaryContainer), - "warning_color": self.rgba_from_argb(dark_theme.secondary), - "warning_bg_color": self.rgba_from_argb(dark_theme.onSecondary), - "warning_fg_color": self.rgba_from_argb(dark_theme.primary, "0.8"), - "error_color": self.rgba_from_argb(dark_theme.error), - "error_bg_color": self.rgba_from_argb(dark_theme.errorContainer), - "error_fg_color": self.rgba_from_argb(dark_theme.onError), - "window_bg_color": self.rgba_from_argb(dark_theme.surface), - "window_fg_color": self.rgba_from_argb(dark_theme.onSurface), - "view_bg_color": self.rgba_from_argb(dark_theme.surface), - "view_fg_color": self.rgba_from_argb(dark_theme.onSurface), - "headerbar_bg_color": self.rgba_from_argb(dark_theme.surface), - "headerbar_fg_color": self.rgba_from_argb(dark_theme.onSurface), - "headerbar_border_color": self.rgba_from_argb( - dark_theme.primary, "0.8" - ), - "headerbar_backdrop_color": "@headerbar_bg_color", - "headerbar_shade_color": self.rgba_from_argb(dark_theme.shadow), - "card_bg_color": self.rgba_from_argb(dark_theme.primary, "0.05"), - "card_fg_color": self.rgba_from_argb(dark_theme.onSecondaryContainer), - "card_shade_color": self.rgba_from_argb(dark_theme.shadow), - "dialog_bg_color": self.rgba_from_argb(dark_theme.secondaryContainer), - "dialog_fg_color": self.rgba_from_argb(dark_theme.onSecondaryContainer), - "popover_bg_color": self.rgba_from_argb(dark_theme.secondaryContainer), - "popover_fg_color": self.rgba_from_argb( - dark_theme.onSecondaryContainer - ), - "shade_color": self.rgba_from_argb(dark_theme.shadow), - "scrollbar_outline_color": self.rgba_from_argb(dark_theme.outline), - } - else: # light - light_theme = theme["schemes"]["light"] - variable = { - "accent_color": self.rgba_from_argb(light_theme.primary), - "accent_bg_color": self.rgba_from_argb(light_theme.primary), - "accent_fg_color": self.rgba_from_argb(light_theme.onPrimary), - "destructive_color": self.rgba_from_argb(light_theme.error), - "destructive_bg_color": self.rgba_from_argb(light_theme.errorContainer), - "destructive_fg_color": self.rgba_from_argb( - light_theme.onErrorContainer - ), - "success_color": self.rgba_from_argb(light_theme.tertiary), - "success_bg_color": self.rgba_from_argb(light_theme.tertiaryContainer), - "success_fg_color": self.rgba_from_argb( - light_theme.onTertiaryContainer - ), - "warning_color": self.rgba_from_argb(light_theme.secondary), - "warning_bg_color": self.rgba_from_argb(light_theme.secondaryContainer), - "warning_fg_color": self.rgba_from_argb( - light_theme.onSecondaryContainer - ), - "error_color": self.rgba_from_argb(light_theme.error), - "error_bg_color": self.rgba_from_argb(light_theme.errorContainer), - "error_fg_color": self.rgba_from_argb(light_theme.onError), - "window_bg_color": self.rgba_from_argb(light_theme.secondaryContainer), - "window_fg_color": self.rgba_from_argb(light_theme.onSurface), - "view_bg_color": self.rgba_from_argb(light_theme.secondaryContainer), - "view_fg_color": self.rgba_from_argb(light_theme.onSurface), - "headerbar_bg_color": self.rgba_from_argb( - light_theme.secondaryContainer - ), - "headerbar_fg_color": self.rgba_from_argb(light_theme.onSurface), - "headerbar_border_color": self.rgba_from_argb( - light_theme.primary, "0.8" - ), - "headerbar_backdrop_color": "@headerbar_bg_color", - "headerbar_shade_color": self.rgba_from_argb( - light_theme.secondaryContainer - ), - "card_bg_color": self.rgba_from_argb(light_theme.primary, "0.05"), - "card_fg_color": self.rgba_from_argb(light_theme.onSecondaryContainer), - "card_shade_color": self.rgba_from_argb(light_theme.shadow), - "dialog_bg_color": self.rgba_from_argb(light_theme.secondaryContainer), - "dialog_fg_color": self.rgba_from_argb( - light_theme.onSecondaryContainer - ), - "popover_bg_color": self.rgba_from_argb(light_theme.secondaryContainer), - "popover_fg_color": self.rgba_from_argb( - light_theme.onSecondaryContainer - ), - "shade_color": self.rgba_from_argb(light_theme.shadow), - "scrollbar_outline_color": self.rgba_from_argb(light_theme.outline), - } + preset_object = PresetUtils().new_preset_from_monet(monet_palette=monet, + props=[tone, monet_theme], obj_only=True) + + variable = preset_object.variables for key in variable: if key in self.pref_variables: @@ -506,16 +405,6 @@ class GradienceApplication(Adw.Application): self.reload_variables() - def generate_gtk_css(self, app_type): - final_css = "" - for key in self.variables.keys(): - final_css += f"@define-color {key} {self.variables[key]};\n" - for prefix_key in self.palette.keys(): - for key in self.palette[prefix_key].keys(): - final_css += f"@define-color {prefix_key + key} {self.palette[prefix_key][key]};\n" - final_css += self.custom_css.get(app_type, "") - return final_css - def mark_as_dirty(self): self.is_dirty = True self.props.active_window.save_preset_button.get_child().set_icon_name( @@ -540,7 +429,7 @@ class GradienceApplication(Adw.Application): def reload_variables(self): parsing_errors = [] - gtk_css = self.generate_gtk_css("gtk4") + gtk_css = PresetUtils().generate_gtk_css("gtk4", self.preset) css_provider = Gtk.CssProvider() def on_error(_, section, error): @@ -616,6 +505,7 @@ class GradienceApplication(Adw.Application): Adw.ResponseAppearance.DESTRUCTIVE, transient_for=self.props.active_window, ) + dialog.gtk3_app_type.set_sensitive(False) dialog.connect("response", self.restore_color_scheme) dialog.present() @@ -782,7 +672,7 @@ class GradienceApplication(Adw.Application): def save_preset(self, _unused, response, preset_entry): if response == "save": - self.preset.save_preset(preset_entry.get_text(), self.plugins_list) + self.preset.save_to_file(preset_entry.get_text(), self.plugins_list) self.clear_dirty() self.win.toast_overlay.add_toast( Adw.Toast(title=_("Preset saved"))) @@ -793,60 +683,10 @@ class GradienceApplication(Adw.Application): def apply_color_scheme(self, widget, response): if response == "apply": if widget.get_app_types()["gtk4"]: - gtk4_dir = os.path.join( - os.environ.get("XDG_CONFIG_HOME", - os.environ["HOME"] + "/.config"), - "gtk-4.0", - ) - if not os.path.exists(gtk4_dir): - os.makedirs(gtk4_dir) - gtk4_css = self.generate_gtk_css("gtk4") - contents = "" - try: - with open( - os.path.join(gtk4_dir, "gtk.css"), "r", encoding="utf-8" - ) as file: - contents = file.read() - except FileNotFoundError: # first run - pass - else: - with open( - os.path.join(gtk4_dir, "gtk.css.bak"), "w", encoding="utf-8" - ) as file: - file.write(contents) - finally: - with open( - os.path.join(gtk4_dir, "gtk.css"), "w", encoding="utf-8" - ) as file: - file.write(gtk4_css) + PresetUtils().apply_preset("gtk4", self.preset) if widget.get_app_types()["gtk3"]: - gtk3_dir = os.path.join( - os.environ.get("XDG_CONFIG_HOME", - os.environ["HOME"] + "/.config"), - "gtk-3.0", - ) - if not os.path.exists(gtk3_dir): - os.makedirs(gtk3_dir) - gtk3_css = self.generate_gtk_css("gtk3") - contents = "" - try: - with open( - os.path.join(gtk3_dir, "gtk.css"), "r", encoding="utf-8" - ) as file: - contents = file.read() - except FileNotFoundError: # first run - pass - else: - with open( - os.path.join(gtk3_dir, "gtk.css.bak"), "w", encoding="utf-8" - ) as file: - file.write(contents) - finally: - with open( - os.path.join(gtk3_dir, "gtk.css"), "w", encoding="utf-8" - ) as file: - file.write(gtk3_css) + PresetUtils().apply_preset("gtk3", self.preset) self.reload_plugins() self.plugins_list.apply() @@ -855,11 +695,12 @@ class GradienceApplication(Adw.Application): Adw.Toast(title=_("Preset set successfully")) ) + # TODO: Make it as a seperate widget dialog = Adw.MessageDialog( transient_for=self.props.active_window, heading=_("Log out"), body=_( - "For the changes to take effect, you need to log out. " + "For the changes to take full effect, you need to log out." ), body_use_markup=True, ) @@ -873,47 +714,14 @@ class GradienceApplication(Adw.Application): def on_theme_set_dialog_response (self, dialog, response): if response == "ok": - print("theme_set_dialog_ok") + logging.debug("theme_set_dialog_ok") def restore_color_scheme(self, widget, response): if response == "restore": if widget.get_app_types()["gtk4"]: - file = Gio.File.new_for_path( - os.path.join( - os.environ.get( - "XDG_CONFIG_HOME", os.environ["HOME"] + "/.config" - ), - "gtk-4.0/gtk.css.bak", - ) - ) try: - backup = open( - os.path.join( - os.environ.get( - "XDG_CONFIG_HOME", os.environ["HOME"] + - "/.config" - ), - "gtk-4.0/gtk.css.bak", - ), - "r", - encoding="utf-8", - ) - contents = backup.read() - backup.close() - gtk4css = open( - os.path.join( - os.environ.get( - "XDG_CONFIG_HOME", os.environ["HOME"] + - "/.config" - ), - "gtk-4.0/gtk.css", - ), - "w", - encoding="utf-8", - ) - gtk4css.write(contents) - gtk4css.close() - except FileNotFoundError: + PresetUtils().restore_gtk4_preset() + except Exception: self.win.toast_overlay.add_toast( Adw.Toast(title=_("Unable to restore GTK 4 backup")) ) @@ -922,7 +730,7 @@ class GradienceApplication(Adw.Application): transient_for=self.props.active_window, heading=_("Log out"), body=_( - "For the changes to take effect, you need to log out. " + "For the changes to take full effect, you need to log out." ), body_use_markup=True, ) @@ -936,37 +744,21 @@ class GradienceApplication(Adw.Application): def on_theme_restore_dialog_response (self, dialog, response): if response == "ok": - print("theme_restore_dialog_ok") + logging.debug("theme_restore_dialog_ok") def reset_color_scheme(self, widget, response): if response == "reset": if widget.get_app_types()["gtk4"]: - file = Gio.File.new_for_path( - os.path.join( - os.environ.get( - "XDG_CONFIG_HOME", os.environ["HOME"] + "/.config" - ), - "gtk-4.0/gtk.css", - ) - ) try: - file.delete() + PresetUtils().reset_preset("gtk4") except Exception: self.win.toast_overlay.add_toast( Adw.Toast(title=_("Unable to delete current preset")) ) if widget.get_app_types()["gtk3"]: - file = Gio.File.new_for_path( - os.path.join( - os.environ.get( - "XDG_CONFIG_HOME", os.environ["HOME"] + "/.config" - ), - "gtk-3.0/gtk.css", - ) - ) try: - file.delete() + PresetUtils().reset_preset("gtk3") except Exception: self.win.toast_overlay.add_toast( Adw.Toast(title=_("Unable to delete current preset")) @@ -976,7 +768,7 @@ class GradienceApplication(Adw.Application): transient_for=self.props.active_window, heading=_("Log out"), body=_( - "For the changes to take effect, you need to log out. " + "For the changes to take full effect, you need to log out." ), body_use_markup=True, ) @@ -990,13 +782,14 @@ class GradienceApplication(Adw.Application): def on_theme_reset_dialog_response (self, dialog, response): if response == "ok": - print("theme_reset_dialog_ok") + logging.debug("theme_reset_dialog_ok") def show_preferences(self, *_args): prefs = GradiencePreferencesWindow(self.win) prefs.set_transient_for(self.win) prefs.present() + # TODO: Move it to seperate frontend module def show_about_window(self, *_args): about = Adw.AboutWindow( transient_for=self.props.active_window, diff --git a/gradience/frontend/meson.build b/gradience/frontend/meson.build index 89be3ed8..2f2f9092 100644 --- a/gradience/frontend/meson.build +++ b/gradience/frontend/meson.build @@ -13,6 +13,7 @@ configure_file( configuration: local_conf ) +subdir('cli') subdir('dialogs') subdir('utils') subdir('views') diff --git a/gradience/frontend/views/main_window.py b/gradience/frontend/views/main_window.py index f01746fd..20bcc552 100644 --- a/gradience/frontend/views/main_window.py +++ b/gradience/frontend/views/main_window.py @@ -23,6 +23,7 @@ from reportlab.graphics import renderPM from material_color_utilities_python import * from gi.repository import Gtk, Adw, Gio +from gradience.backend.theming.monet import Monet from gradience.backend.constants import rootdir, app_id, build_type from gradience.frontend.widgets.error_list_row import GradienceErrorListRow @@ -70,12 +71,12 @@ class GradienceMainWindow(Adw.ApplicationWindow): self.connect("unrealize", self.save_window_props) self.style_manager = self.get_application().style_manager - self.first_apply = True + #self.first_apply = True self.get_default_wallpaper() - # FIXME: This function works only when building using meson, because Flatpak - # can't access host's dconf with current config/impl + # TODO: Check if org.freedesktop.portal.Settings portal will allow us to \ + # read org.gnome.desktop.background DConf key def get_default_wallpaper(self): background_settings = Gio.Settings("org.gnome.desktop.background") if self.style_manager.get_dark(): @@ -210,39 +211,23 @@ class GradienceMainWindow(Adw.ApplicationWindow): def on_apply_button(self, *_args): if self.monet_image_file: - if self.monet_image_file.endswith(".svg"): - drawing = svg2rlg(self.monet_image_file) - self.monet_image_file = os.path.join( - os.environ.get("XDG_RUNTIME_DIR"), "gradience_bg.png" - ) - renderPM.drawToFile(drawing, self.monet_image_file, fmt="PNG") - - if self.monet_image_file.endswith(".xml"): - logging.debug("XML WIP") - try: - self.monet_img = Image.open(self.monet_image_file) - except Exception: - self.toast_overlay.add_toast( - Adw.Toast(title=_("Unsupported background type")) - ) - else: - basewidth = 64 - wpercent = basewidth / float(self.monet_img.size[0]) - hsize = int((float(self.monet_img.size[1]) * float(wpercent))) - self.monet_img = self.monet_img.resize( - (basewidth, hsize), Image.Resampling.LANCZOS - ) - self.theme = themeFromImage(self.monet_img) + self.theme = Monet().generate_from_image(self.monet_image_file) + self.tone = self.tone_row.get_selected_item() self.monet_theme = self.monet_theme_row.get_selected_item() self.get_application().update_theme_from_monet( self.theme, self.tone, self.monet_theme ) - if not self.first_apply: - self.toast_overlay.add_toast( - Adw.Toast(title=_("Palette generated")) - ) + except Exception as e: + logging.error(f"Failed to generate Monet palette. Exc: {e}") + self.toast_overlay.add_toast( + Adw.Toast(title=_("Failed to generate Monet palette")) + ) + else: + self.toast_overlay.add_toast( + Adw.Toast(title=_("Palette generated")) + ) else: self.toast_overlay.add_toast( Adw.Toast(title=_("Select a background first")) diff --git a/gradience/frontend/views/preferences_window.py b/gradience/frontend/views/preferences_window.py index 0c819d6a..b6d59a51 100644 --- a/gradience/frontend/views/preferences_window.py +++ b/gradience/frontend/views/preferences_window.py @@ -84,9 +84,9 @@ class GradiencePreferencesWindow(Adw.PreferencesWindow): state = self.allow_gtk4_flatpak_theming_user.props.state if not state: - create_gtk_user_override(self, self.settings, "gtk4") + create_gtk_user_override(self.settings, "gtk4", self) else: - remove_gtk_user_override(self, self.settings, "gtk4") + remove_gtk_user_override(self.settings, "gtk4", self) logging.debug( f"user-flatpak-theming-gtk4: {self.settings.get_boolean('user-flatpak-theming-gtk4')}" @@ -96,9 +96,9 @@ class GradiencePreferencesWindow(Adw.PreferencesWindow): state = self.allow_gtk3_flatpak_theming_user.props.state if not state: - create_gtk_user_override(self, self.settings, "gtk3") + create_gtk_user_override(self.settings, "gtk3", self) else: - remove_gtk_user_override(self, self.settings, "gtk3") + remove_gtk_user_override(self.settings, "gtk3", self) logging.debug( f"user-flatpak-theming-gtk3: {self.settings.get_boolean('user-flatpak-theming-gtk3')}" @@ -108,9 +108,9 @@ class GradiencePreferencesWindow(Adw.PreferencesWindow): state = self.allow_gtk4_flatpak_theming_global.props.state if not state: - create_gtk_global_override(self, self.settings, "gtk4") + create_gtk_global_override(self.settings, "gtk4", self) else: - remove_gtk_global_override(self, self.settings, "gtk4") + remove_gtk_global_override(self.settings, "gtk4", self) logging.debug( f"global-flatpak-theming-gtk4: {self.settings.get_boolean('global-flatpak-theming-gtk4')}" @@ -120,9 +120,9 @@ class GradiencePreferencesWindow(Adw.PreferencesWindow): state = self.allow_gtk3_flatpak_theming_global.props.state if not state: - create_gtk_global_override(self, self.settings, "gtk3") + create_gtk_global_override(self.settings, "gtk3", self) else: - remove_gtk_global_override(self, self.settings, "gtk3") + remove_gtk_global_override(self.settings, "gtk3", self) logging.debug( f"global-flatpak-theming-gtk3: {self.settings.get_boolean('global-flatpak-theming-gtk3')}" diff --git a/gradience/frontend/views/presets_manager_window.py b/gradience/frontend/views/presets_manager_window.py index eb7720b5..e365fbc0 100644 --- a/gradience/frontend/views/presets_manager_window.py +++ b/gradience/frontend/views/presets_manager_window.py @@ -24,8 +24,8 @@ from collections import OrderedDict from pathlib import Path from gi.repository import Gtk, Adw, GLib -from gradience.backend.preset_downloader import fetch_presets -from gradience.backend.models.preset import presets_dir +from gradience.backend.preset_downloader import PresetDownloader +from gradience.backend.globals import presets_dir, preset_repos from gradience.backend.constants import rootdir from gradience.frontend.widgets.preset_row import GradiencePresetRow @@ -65,11 +65,6 @@ class GradiencePresetWindow(Adw.Window): custom_presets = {} - official_repositories = { - "Official": "https://github.com/GradienceTeam/Community/raw/next/official.json", - "Curated": "https://github.com/GradienceTeam/Community/raw/next/curated.json", - } - search_results_list = [] offline = False @@ -146,9 +141,19 @@ class GradiencePresetWindow(Adw.Window): else: badge = "white" - explore_presets, urls = fetch_presets(repo) - - if explore_presets: + try: + explore_presets, urls = PresetDownloader().fetch_presets(repo) + except GLib.GError as e: + if e.code == 1: + self.offline = True + self.search_spinner.props.visible = False + self.search_stack.set_visible_child_name("page_offline") + else: + self.search_spinner.props.visible = False + # TODO: Create a new page to show for other errors eg. "page_error" + except json.JSONDecodeError as e: + self.search_spinner.props.visible = False + else: self.search_spinner.props.visible = False for (preset, preset_name), preset_url in zip( @@ -159,10 +164,6 @@ class GradiencePresetWindow(Adw.Window): ) self.search_results.append(row) self.search_results_list.append(row) - else: - self.offline = True - self.search_spinner.props.visible = False - self.search_stack.set_visible_child_name("page_offline") def add_repo(self, _unused, response, name_entry, url_entry): if response == "add": @@ -315,6 +316,7 @@ class GradiencePresetWindow(Adw.Window): "pretty-purple": "Pretty Purple", } + # TODO: Move this from frontend to backend for repo in Path(presets_dir).iterdir(): logging.debug(f"presets_dir.iterdir: {repo}") if repo.is_dir(): # repo @@ -426,7 +428,7 @@ class GradiencePresetWindow(Adw.Window): self.repos_list = Adw.PreferencesGroup() self.repos_list.set_title(_("Repositories")) - for repo_name, repo in self.official_repositories.items(): + for repo_name, repo in preset_repos.items(): row = GradienceRepoRow(repo, repo_name, self, deletable=False) self.repos_list.add(row) @@ -436,4 +438,4 @@ class GradiencePresetWindow(Adw.Window): self.repos.add(self.repos_list) - self._repos = {**self.user_repositories, **self.official_repositories} + self._repos = {**self.user_repositories, **preset_repos} diff --git a/gradience/frontend/views/welcome_window.py b/gradience/frontend/views/welcome_window.py index dde8917a..e97fb086 100644 --- a/gradience/frontend/views/welcome_window.py +++ b/gradience/frontend/views/welcome_window.py @@ -176,7 +176,7 @@ class GradienceWelcomeWindow(Adw.Window): self.allow_flatpak_theming_user_toggled() def allow_flatpak_theming_user_toggled(self, *args): - create_gtk_user_override(self, self.gio_settings, "gtk4") + create_gtk_user_override(self.gio_settings, "gtk4", self) def install_runner(self, widget): def set_completed(result, error=False): diff --git a/gradience/frontend/widgets/explore_preset_row.py b/gradience/frontend/widgets/explore_preset_row.py index fb9c5554..cba7c405 100644 --- a/gradience/frontend/widgets/explore_preset_row.py +++ b/gradience/frontend/widgets/explore_preset_row.py @@ -21,7 +21,7 @@ import os from gi.repository import Gtk, Adw from gradience.backend.utils.common import to_slug_case -from gradience.backend.preset_downloader import download_preset +from gradience.backend.preset_downloader import PresetDownloader from gradience.backend.constants import rootdir from gradience.backend.logger import Logger @@ -60,7 +60,7 @@ class GradienceExplorePresetRow(Adw.ActionRow): @Gtk.Template.Callback() def on_apply_button_clicked(self, *_args): try: - download_preset(to_slug_case(self.name), self.prefix, self.url) + PresetDownloader().download_preset(to_slug_case(self.name), self.prefix, self.url) except Exception as e: self.toast_overlay.add_toast( Adw.Toast(title=_("Preset could not be downloaded")) @@ -86,7 +86,7 @@ class GradienceExplorePresetRow(Adw.ActionRow): @Gtk.Template.Callback() def on_download_button_clicked(self, *_args): try: - download_preset(to_slug_case(self.name), self.prefix, self.url) + PresetDownloader().download_preset(to_slug_case(self.name), self.prefix, self.url) except Exception as e: self.toast_overlay.add_toast( Adw.Toast(title=_("Preset could not be downloaded")) diff --git a/gradience/frontend/widgets/preset_row.py b/gradience/frontend/widgets/preset_row.py index 4cc831ef..6999055c 100644 --- a/gradience/frontend/widgets/preset_row.py +++ b/gradience/frontend/widgets/preset_row.py @@ -22,7 +22,7 @@ from gi.repository import Gtk, Adw, Xdp, XdpGtk4 from gradience.frontend.views.share_window import GradienceShareWindow from gradience.backend.utils.common import to_slug_case -from gradience.backend.models.preset import Preset, presets_dir +from gradience.backend.models.preset import Preset from gradience.backend.constants import rootdir from gradience.backend.logger import Logger @@ -61,7 +61,7 @@ class GradiencePresetRow(Adw.ExpanderRow): self.win = win self.toast_overlay = self.win.toast_overlay - self.preset = Preset(preset_path) + self.preset = Preset().new_from_path(preset_path) if self.preset.badges: self.has_badges = True @@ -124,7 +124,7 @@ class GradiencePresetRow(Adw.ExpanderRow): if self.name_entry_toggle.get_active(): self.value_stack.set_visible_child(self.name_entry) else: - self.preset.rename_preset(self.name_entry.get_text()) + self.preset.rename(self.name_entry.get_text()) self.value_stack.set_visible_child(self.apply_button) def on_report_btn_clicked(self, *_args): diff --git a/local_cli.sh b/local_cli.sh new file mode 100755 index 00000000..78803dc3 --- /dev/null +++ b/local_cli.sh @@ -0,0 +1,21 @@ +#!/usr/bin/bash + +# local_cli.sh +# +# Change the look of Adwaita, with ease +# Copyright (C) 2022 Gradience Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +python builddir/gradience/frontend/gradience-cli "$@"