From e834e55b11bfe98fba2972a08f90d443749a8528 Mon Sep 17 00:00:00 2001 From: tfuxu <73042332+tfuxu@users.noreply.github.com> Date: Thu, 8 Dec 2022 12:43:12 +0100 Subject: [PATCH 01/24] backend/models: refactor Preset model object This commit is the first in a series of commits that will introduce new CLI interface for Gradience. Currently we need to move some parts of code that reside mostly in frontend modules to their new backend modules, and this commit is the beggining of this whole ordeal. * Refactor Preset model object, in order to make it more readable and to allow `new` function that directly takes new values for properties * Move presets_dir from models/preset to new backend/globals module * Create new utils/colors module (will be used in later commits) * Update all modules that are affected by above changes --- gradience/backend/globals.py | 25 +++ gradience/backend/meson.build | 1 + gradience/backend/models/preset.py | 154 ++++++++++++++---- gradience/backend/models/repo.py | 6 +- gradience/backend/preset_downloader.py | 2 +- gradience/backend/utils/colors.py | 31 ++++ gradience/backend/utils/meson.build | 1 + gradience/frontend/main.py | 35 ++-- .../frontend/views/presets_manager_window.py | 2 +- gradience/frontend/widgets/preset_row.py | 6 +- 10 files changed, 201 insertions(+), 62 deletions(-) create mode 100644 gradience/backend/globals.py create mode 100644 gradience/backend/utils/colors.py diff --git a/gradience/backend/globals.py b/gradience/backend/globals.py new file mode 100644 index 00000000..5264ef96 --- /dev/null +++ b/gradience/backend/globals.py @@ -0,0 +1,25 @@ +# 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 + + +presets_dir = os.path.join( + os.environ.get("XDG_CONFIG_HOME", os.environ["HOME"] + "/.config"), + "presets", +) diff --git a/gradience/backend/meson.build b/gradience/backend/meson.build index 874407da..ef07155d 100644 --- a/gradience/backend/meson.build +++ b/gradience/backend/meson.build @@ -26,6 +26,7 @@ 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..9614923b 100644 --- a/gradience/backend/models/preset.py +++ b/gradience/backend/models/preset.py @@ -21,21 +21,84 @@ 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", + } +} class Preset: variables = {} - palette = {} + palette = adw_palette custom_css = { "gtk4": "", "gtk3": "", @@ -45,31 +108,58 @@ class Preset: preset_path = "new_preset" 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, display_name: str, variables: dict, palette=None, custom_css=None, badges=None): + self.display_name = display_name + self.variables = variables + + 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"] @@ -85,24 +175,21 @@ class Preset: for app_type in settings_schema["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) # 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: @@ -141,5 +228,6 @@ class Preset: file.write(json.dumps(object_to_write, indent=4)) file.close() + # 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..8f697adb 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 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/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/main.py b/gradience/frontend/main.py index f07e3fa2..11644875 100644 --- a/gradience/frontend/main.py +++ b/gradience/frontend/main.py @@ -25,8 +25,10 @@ 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.utils.colors import rgba_from_argb from gradience.backend.utils.common import to_slug_case from gradience.backend.constants import * @@ -288,7 +290,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 +322,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,29 +376,17 @@ 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" @@ -506,6 +496,7 @@ class GradienceApplication(Adw.Application): self.reload_variables() + # TODO: Move to backend/utils modules def generate_gtk_css(self, app_type): final_css = "" for key in self.variables.keys(): @@ -782,7 +773,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"))) diff --git a/gradience/frontend/views/presets_manager_window.py b/gradience/frontend/views/presets_manager_window.py index eb7720b5..1f23072a 100644 --- a/gradience/frontend/views/presets_manager_window.py +++ b/gradience/frontend/views/presets_manager_window.py @@ -25,7 +25,7 @@ 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.globals import presets_dir from gradience.backend.constants import rootdir from gradience.frontend.widgets.preset_row import GradiencePresetRow 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): From 22f476f76602c617a15e90e590587fe16be6b89f Mon Sep 17 00:00:00 2001 From: tfuxu <73042332+tfuxu@users.noreply.github.com> Date: Thu, 8 Dec 2022 14:05:38 +0100 Subject: [PATCH 02/24] backend: create new modules for preset generation utilities --- gradience/backend/theming/__init__.py | 0 gradience/backend/theming/meson.build | 8 + gradience/backend/theming/monet.py | 68 +++++++++ gradience/backend/theming/preset_utils.py | 174 ++++++++++++++++++++++ gradience/backend/utils/common.py | 2 +- gradience/frontend/main.py | 105 +------------ gradience/frontend/views/main_window.py | 43 ++---- 7 files changed, 271 insertions(+), 129 deletions(-) create mode 100644 gradience/backend/theming/__init__.py create mode 100644 gradience/backend/theming/meson.build create mode 100644 gradience/backend/theming/monet.py create mode 100644 gradience/backend/theming/preset_utils.py 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..32c5a2fb --- /dev/null +++ b/gradience/backend/theming/monet.py @@ -0,0 +1,68 @@ +# 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) -> dict: + #TODO: Test SVG support? I don't know what's that gradience_bg.png / + # and why it is used for SVG images + 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 + + def palette_to_dict(self, palette): + pass diff --git a/gradience/backend/theming/preset_utils.py b/gradience/backend/theming/preset_utils.py new file mode 100644 index 00000000..05cc0750 --- /dev/null +++ b/gradience/backend/theming/preset_utils.py @@ -0,0 +1,174 @@ +# 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 json +import material_color_utilities_python as monet + +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.logger import Logger + +logging = Logger() + + +class PresetUtils: + def __init__(self): + self.preset = Preset() + + def new_preset_from_monet(self, name=None, monet_palette=None, props=None, vars_only=False) -> dict or bool: + 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 vars_only == False and not name: + raise Exception("You either need to set 'vars_only' property to True, or add value to 'name' property") + + if vars_only: + return variable + + self.preset.new(display_name=name, variables=variable) + + '''preset_dict = { + "name": self.preset.display_name, + "variables": self.preset.variables, + "palette": self.preset.palette, + "custom_css": self.preset.custom_css, + "plugins": self.preset.plugins, + } + logging.debug("Generated Monet preset:\n" + json.dumps(preset_dict, indent=4))''' + + try: + self.preset.save_to_file(name=name) + except Exception as e: + # TODO: Move exception handling to model/preset module + logging.error(f"Unexpected file error while trying to generate preset from generated Monet palette. Exc: {e}") + return False + + return True + +if __name__ == "__main__": + preset_utils = PresetUtils() + + monet_palette = Monet().generate_from_image("/home/tfuxc/Pictures/Wallpapers/wallhaven-57kzw1.png") + props = [20, "dark"] + + preset_utils.new_preset_from_monet("My awesome Monet", monet_palette, props) 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/frontend/main.py b/gradience/frontend/main.py index 11644875..0d9a0e2f 100644 --- a/gradience/frontend/main.py +++ b/gradience/frontend/main.py @@ -28,6 +28,7 @@ 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 +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 * @@ -393,102 +394,8 @@ class GradienceApplication(Adw.Application): 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), - } + variable = PresetUtils().new_preset_from_monet(monet_palette=monet, + props=[tone, monet_theme], vars_only=True) for key in variable: if key in self.pref_variables: @@ -864,7 +771,7 @@ 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": @@ -927,7 +834,7 @@ 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": @@ -981,7 +888,7 @@ 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) diff --git a/gradience/frontend/views/main_window.py b/gradience/frontend/views/main_window.py index f01746fd..4151b196 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,11 +71,11 @@ 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 + # FIXME: This function works only when building using meson, because Flatpak \ # can't access host's dconf with current config/impl def get_default_wallpaper(self): background_settings = Gio.Settings("org.gnome.desktop.background") @@ -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")) From e1e244db72a90203ccec1d94c1a5de75041c843c Mon Sep 17 00:00:00 2001 From: tfuxu <73042332+tfuxu@users.noreply.github.com> Date: Fri, 9 Dec 2022 00:12:02 +0100 Subject: [PATCH 03/24] backend/theming: move apply, restore and reset preset functions to backend --- gradience/backend/globals.py | 16 +++ gradience/backend/theming/preset_utils.py | 146 +++++++++++++++++++++- gradience/frontend/main.py | 120 ++---------------- 3 files changed, 172 insertions(+), 110 deletions(-) diff --git a/gradience/backend/globals.py b/gradience/backend/globals.py index 5264ef96..ff26f4e3 100644 --- a/gradience/backend/globals.py +++ b/gradience/backend/globals.py @@ -23,3 +23,19 @@ presets_dir = os.path.join( os.environ.get("XDG_CONFIG_HOME", os.environ["HOME"] + "/.config"), "presets", ) + +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 diff --git a/gradience/backend/theming/preset_utils.py b/gradience/backend/theming/preset_utils.py index 05cc0750..6c81f414 100644 --- a/gradience/backend/theming/preset_utils.py +++ b/gradience/backend/theming/preset_utils.py @@ -16,13 +16,17 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import os import json -import material_color_utilities_python as monet + +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 get_gtk_theme_dir + from gradience.backend.logger import Logger logging = Logger() @@ -32,6 +36,24 @@ 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, vars_only=False) -> dict or bool: if props: tone = props[0] @@ -160,11 +182,131 @@ class PresetUtils: self.preset.save_to_file(name=name) except Exception as e: # TODO: Move exception handling to model/preset module - logging.error(f"Unexpected file error while trying to generate preset from generated Monet palette. Exc: {e}") + logging.error(f"Unexpected file error while trying to generate preset from Monet palette. Exc: {e}") return False return True + 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): + 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 + + if __name__ == "__main__": preset_utils = PresetUtils() diff --git a/gradience/frontend/main.py b/gradience/frontend/main.py index 0d9a0e2f..b46d1695 100644 --- a/gradience/frontend/main.py +++ b/gradience/frontend/main.py @@ -514,6 +514,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() @@ -691,60 +692,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() @@ -753,11 +704,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, ) @@ -776,42 +728,9 @@ class GradienceApplication(Adw.Application): 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")) ) @@ -820,7 +739,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, ) @@ -839,32 +758,16 @@ class GradienceApplication(Adw.Application): 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")) @@ -874,7 +777,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, ) @@ -895,6 +798,7 @@ class GradienceApplication(Adw.Application): 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, From 674848edaa4d2019d19056d28d11a13636902b02 Mon Sep 17 00:00:00 2001 From: tfuxu <73042332+tfuxu@users.noreply.github.com> Date: Sat, 10 Dec 2022 14:58:49 +0100 Subject: [PATCH 04/24] backend: include backend/theming modules in meson compilation --- gradience/backend/meson.build | 1 + 1 file changed, 1 insertion(+) diff --git a/gradience/backend/meson.build b/gradience/backend/meson.build index ef07155d..90f2734b 100644 --- a/gradience/backend/meson.build +++ b/gradience/backend/meson.build @@ -20,6 +20,7 @@ configure_file( ) subdir('models') +subdir('theming') subdir('utils') gradience_sources = [ From 527a9dc90fbe5d18012c4b92207f365e5aaf8d6d Mon Sep 17 00:00:00 2001 From: tfuxu <73042332+tfuxu@users.noreply.github.com> Date: Sat, 10 Dec 2022 15:21:13 +0100 Subject: [PATCH 05/24] backend: seperate JSON encoding in Preset module to a new function * add more checks in new_preset_from_monet function * return full Preset object instead of dict data * remove some unused code --- gradience/backend/models/preset.py | 34 +++++++++------ gradience/backend/theming/monet.py | 5 +-- gradience/backend/theming/preset_utils.py | 52 +++++++++-------------- gradience/frontend/main.py | 19 +++------ 4 files changed, 46 insertions(+), 64 deletions(-) diff --git a/gradience/backend/models/preset.py b/gradience/backend/models/preset.py index 9614923b..3736ec5b 100644 --- a/gradience/backend/models/preset.py +++ b/gradience/backend/models/preset.py @@ -106,15 +106,17 @@ class Preset: plugins = {} display_name = "New Preset" preset_path = "new_preset" + plugins_list = {} badges = {} def __init__(self): pass - def new(self, display_name: str, variables: dict, palette=None, custom_css=None, badges=None): - self.display_name = display_name + 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: @@ -188,12 +190,24 @@ class Preset: 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_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: @@ -212,20 +226,12 @@ class Preset: ) ) - 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)) + content = self.get_preset_json(indent=4) + file.write(content) file.close() # TODO: Add validation diff --git a/gradience/backend/theming/monet.py b/gradience/backend/theming/monet.py index 32c5a2fb..3efa8892 100644 --- a/gradience/backend/theming/monet.py +++ b/gradience/backend/theming/monet.py @@ -33,7 +33,7 @@ class Monet: def __init__(self): self.palette = None - def generate_from_image(self, image_path) -> dict: + def generate_from_image(self, image_path: str) -> dict: #TODO: Test SVG support? I don't know what's that gradience_bg.png / # and why it is used for SVG images if image_path.endswith(".svg"): @@ -63,6 +63,3 @@ class Monet: self.palette = monet.themeFromImage(monet_img) return self.palette - - def palette_to_dict(self, palette): - pass diff --git a/gradience/backend/theming/preset_utils.py b/gradience/backend/theming/preset_utils.py index 6c81f414..16c8f99c 100644 --- a/gradience/backend/theming/preset_utils.py +++ b/gradience/backend/theming/preset_utils.py @@ -54,7 +54,7 @@ class PresetUtils: return final_css - def new_preset_from_monet(self, name=None, monet_palette=None, props=None, vars_only=False) -> dict or bool: + 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] @@ -161,31 +161,28 @@ class PresetUtils: "scrollbar_outline_color": rgba_from_argb(light_theme.outline), } - if vars_only == False and not name: - raise Exception("You either need to set 'vars_only' property to True, or add value to 'name' property") + 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 vars_only: - return variable + 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 - self.preset.new(display_name=name, variables=variable) + if obj_only == False: + print("no obj_only, name") + self.preset.new(variables=variable, display_name=name) - '''preset_dict = { - "name": self.preset.display_name, - "variables": self.preset.variables, - "palette": self.preset.palette, - "custom_css": self.preset.custom_css, - "plugins": self.preset.plugins, - } - logging.debug("Generated Monet preset:\n" + json.dumps(preset_dict, indent=4))''' - - try: - self.preset.save_to_file(name=name) - 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}") - return False - - return True + 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 apply_preset(self, app_type: str, preset: Preset) -> None: if app_type == "gtk4": @@ -305,12 +302,3 @@ class PresetUtils: except GLib.GError as e: logging.error(f"Unable to delete current preset. Exc: {e}") raise - - -if __name__ == "__main__": - preset_utils = PresetUtils() - - monet_palette = Monet().generate_from_image("/home/tfuxc/Pictures/Wallpapers/wallhaven-57kzw1.png") - props = [20, "dark"] - - preset_utils.new_preset_from_monet("My awesome Monet", monet_palette, props) diff --git a/gradience/frontend/main.py b/gradience/frontend/main.py index b46d1695..08efada0 100644 --- a/gradience/frontend/main.py +++ b/gradience/frontend/main.py @@ -394,8 +394,10 @@ class GradienceApplication(Adw.Application): else: monet_theme = "light" - variable = PresetUtils().new_preset_from_monet(monet_palette=monet, - props=[tone, monet_theme], vars_only=True) + 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: @@ -403,17 +405,6 @@ class GradienceApplication(Adw.Application): self.reload_variables() - # TODO: Move to backend/utils modules - 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( @@ -438,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): From f0afbd817de9bf649fefe216749a4601b51be1a2 Mon Sep 17 00:00:00 2001 From: tfuxu <73042332+tfuxu@users.noreply.github.com> Date: Sat, 10 Dec 2022 16:55:42 +0100 Subject: [PATCH 06/24] frontend: introduce initial CLI interface This commit adds a new CLI interface made using argparse library. Current status of CLI interface is very WIP, as it lacks logic for the majority of commands and doesn't work properly on Flatpak and local builds. Currently working commands: monet, version --- gradience/frontend/cli/__init__.py | 0 gradience/frontend/cli/cli.in | 170 +++++++++++++++++++++++++++++ gradience/frontend/cli/meson.build | 14 +++ gradience/frontend/meson.build | 1 + 4 files changed, 185 insertions(+) create mode 100644 gradience/frontend/cli/__init__.py create mode 100755 gradience/frontend/cli/cli.in create mode 100644 gradience/frontend/cli/meson.build 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..19683898 --- /dev/null +++ b/gradience/frontend/cli/cli.in @@ -0,0 +1,170 @@ +#!/usr/bin/env python3 + +# cli.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 sys +import json +import signal +import argparse + +from gradience.backend.theming.monet import Monet +from gradience.backend.models.preset import Preset +from gradience.backend.theming.preset_utils import PresetUtils + + +version = "@VERSION@" + +signal.signal(signal.SIGINT, signal.SIG_DFL) + + +class CLI: + 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', '--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") + + favorites_parser = subparsers.add_parser("favorites", help="list favorited presets") + favorites_parser.add_argument("-a", "--add-preset", metavar="PRESET_NAME", help="add preset to favorites") + favorites_parser.add_argument("-r", "--remove-preset", metavar="PRESET_NAME", help="remove preset from favorites") + + apply_parser = subparsers.add_parser("apply", help="apply an preset") + apply_group = apply_parser.add_mutually_exclusive_group(required=True) + apply_group.add_argument("-n", "--preset-name", help="preset's display name") + 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="preset's display name", 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("--preset-stdout", action="store_true", help="print out preset in JSON format directly to stdout") + + download_parser = subparsers.add_parser("download", help="download preset from internet") + #new_parser.add_argument("-i", "--interactive", action="store_true", help="") + download_parser.add_argument("-n", "--preset-name", help="", 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 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="set 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("--preset-stdout", action="store_true", help="print out preset in JSON format directly to stdout") + + 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", action="store_true", help="enable overrides for Flatpak theming") + overrides_group.add_argument("-d", "--disable-theming", action="store_true", 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 == "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 == "flatpak-overrides": + self.flatpak_theming(args) + + def list_presets(self, args): + pass + + def favorite_presets(self, args): + pass + + def apply_preset(self, args): + _preset_name = args.preset_name + _preset_path = args.preset_path + _gtk = args.gtk + _flatpak = args.flatpak + + print(_preset_name) + + def new_preset(self, args): + #_interactive = args.interactive + _name = args.name + _colors = args.colors + _palette = args.palette + _custom_css = args.custom_css + _preset_stdout = args.preset_stdout + + def download_preset(self, args): + #_interactive = args.interactive + _preset_name = args.preset_name + #_custom_url = args.custom_url + + def generate_monet(self, args): + _preset_name = args.preset_name + _image_path = args.image_path + _tone = args.tone + _theme = args.theme + _preset_stdout = args.preset_stdout + + palette = Monet().generate_from_image(_image_path) + props = [_tone, _theme] + + if _preset_stdout: + preset = PresetUtils().new_preset_from_monet(name=_preset_name, monet_palette=palette, props=props, obj_only=True) + preset_json = preset.get_preset_json() + + return print(preset_json) + + monet_preset = PresetUtils().new_preset_from_monet(_preset_name, palette, props) + + def flatpak_theming(self, args): + _enable_theming = args.enable_theming + _disable_theming = args.disable_theming + + +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..a5ee8000 --- /dev/null +++ b/gradience/frontend/cli/meson.build @@ -0,0 +1,14 @@ +clidir = 'gradience/frontend/cli' + +configure_file( + input: 'cli.in', + output: 'gradience-cli', + configuration: conf, + install: true, + install_dir: get_option('bindir') +) + +gradience_sources = [ + '__init__.py' +] +PY_INSTALLDIR.install_sources(gradience_sources, subdir: clidir) 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') From c74bd9bfe24def2aadfebee7ba19bac15e0ba3e8 Mon Sep 17 00:00:00 2001 From: tfuxu <73042332+tfuxu@users.noreply.github.com> Date: Sun, 11 Dec 2022 12:53:18 +0100 Subject: [PATCH 07/24] backend: allow functions in flatpak_override module to be used without specifying an Adw.ToastOverlay object --- gradience/backend/flatpak_overrides.py | 84 +++++++++---------- .../frontend/views/preferences_window.py | 16 ++-- gradience/frontend/views/welcome_window.py | 2 +- 3 files changed, 51 insertions(+), 51 deletions(-) diff --git a/gradience/backend/flatpak_overrides.py b/gradience/backend/flatpak_overrides.py index 666d9ae9..e57674a8 100644 --- a/gradience/backend/flatpak_overrides.py +++ b/gradience/backend/flatpak_overrides.py @@ -60,11 +60,12 @@ 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(settings, user_keyfile, filename, gtk_ver, 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": @@ -79,11 +80,12 @@ def user_save_keyfile(toast_overlay, settings, user_keyfile, filename, gtk_ver): ) -def global_save_keyfile(toast_overlay, settings, global_keyfile, filename, gtk_ver): +def global_save_keyfile(settings, global_keyfile, filename, gtk_ver, 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": @@ -101,7 +103,7 @@ def global_save_keyfile(toast_overlay, settings, global_keyfile, filename, gtk_v """ Main functions """ -def create_gtk_user_override(toast_overlay, settings, gtk_ver): +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}") @@ -147,12 +149,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(settings, user_keyfile, + filename, 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 +163,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(settings, user_keyfile, + filename, 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(settings, user_keyfile, + filename, gtk_ver, toast_overlay) else: if is_gtk4: settings.set_boolean("user-flatpak-theming-gtk4", True) @@ -178,7 +180,7 @@ 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): +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}") @@ -210,9 +212,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 +232,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(settings, user_keyfile, + filename, gtk_ver, toast_overlay) logging.debug("remove override: Value removed.") else: set_theming() @@ -242,7 +244,7 @@ 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"]) logging.debug(f"override_dir: {override_dir}") @@ -289,13 +291,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(settings, global_keyfile, + filename, 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 +305,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(settings, global_keyfile, + filename, 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(settings, global_keyfile, + filename, gtk_ver, toast_overlay) else: if is_gtk4: settings.set_boolean("global-flatpak-theming-gtk4", True) @@ -322,7 +322,7 @@ 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"]) logging.debug(f"override_dir: {override_dir}") @@ -355,9 +355,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 +375,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(settings, global_keyfile, + filename, gtk_ver, toast_overlay) logging.debug("remove override: Value removed.") else: set_theming() 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/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): From 24d34296de338ef210348864c47bdaad79ff1ae5 Mon Sep 17 00:00:00 2001 From: tfuxu <73042332+tfuxu@users.noreply.github.com> Date: Sun, 11 Dec 2022 12:57:30 +0100 Subject: [PATCH 08/24] backend/globals: move official Gradience preset repositories list to globals module --- gradience/backend/globals.py | 11 ++++++++--- gradience/frontend/views/presets_manager_window.py | 11 +++-------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/gradience/backend/globals.py b/gradience/backend/globals.py index ff26f4e3..08552452 100644 --- a/gradience/backend/globals.py +++ b/gradience/backend/globals.py @@ -21,21 +21,26 @@ import os presets_dir = os.path.join( os.environ.get("XDG_CONFIG_HOME", os.environ["HOME"] + "/.config"), - "presets", + "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", + "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", + "gtk-3.0" ) return theme_dir diff --git a/gradience/frontend/views/presets_manager_window.py b/gradience/frontend/views/presets_manager_window.py index 1f23072a..7eb27e17 100644 --- a/gradience/frontend/views/presets_manager_window.py +++ b/gradience/frontend/views/presets_manager_window.py @@ -25,7 +25,7 @@ from pathlib import Path from gi.repository import Gtk, Adw, GLib from gradience.backend.preset_downloader import fetch_presets -from gradience.backend.globals import presets_dir +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 @@ -426,7 +421,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 +431,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} From 7c1a29561d899312b99063cbc3f6423ac4cf24f7 Mon Sep 17 00:00:00 2001 From: tfuxu <73042332+tfuxu@users.noreply.github.com> Date: Sun, 11 Dec 2022 13:23:12 +0100 Subject: [PATCH 09/24] frontend/cli: add rudimentary logic code for apply, download and flatpak_theming commands --- gradience/backend/theming/monet.py | 2 - gradience/backend/theming/preset_utils.py | 2 +- gradience/frontend/cli/cli.in | 76 ++++++++++++++++++++--- 3 files changed, 68 insertions(+), 12 deletions(-) diff --git a/gradience/backend/theming/monet.py b/gradience/backend/theming/monet.py index 3efa8892..057f4dd8 100644 --- a/gradience/backend/theming/monet.py +++ b/gradience/backend/theming/monet.py @@ -34,8 +34,6 @@ class Monet: self.palette = None def generate_from_image(self, image_path: str) -> dict: - #TODO: Test SVG support? I don't know what's that gradience_bg.png / - # and why it is used for SVG images if image_path.endswith(".svg"): drawing = svg2rlg(image_path) image_path = os.path.join( diff --git a/gradience/backend/theming/preset_utils.py b/gradience/backend/theming/preset_utils.py index 16c8f99c..89bbd589 100644 --- a/gradience/backend/theming/preset_utils.py +++ b/gradience/backend/theming/preset_utils.py @@ -238,7 +238,7 @@ class PresetUtils: ) as file: file.write(gtk3_css) - def restore_gtk4_preset(self): + def restore_gtk4_preset(self) -> None: try: with open( os.path.join( diff --git a/gradience/frontend/cli/cli.in b/gradience/frontend/cli/cli.in index 19683898..83eb0ac5 100755 --- a/gradience/frontend/cli/cli.in +++ b/gradience/frontend/cli/cli.in @@ -22,10 +22,18 @@ import sys import json import signal import argparse +import warnings + +warnings.filterwarnings("ignore") # suppress GTK warnings +from gi.repository import Gio + +from gradience.backend.globals import preset_repos 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 fetch_presets, download_preset +from gradience.backend.flatpak_overrides import create_gtk_user_override, remove_gtk_user_override version = "@VERSION@" @@ -34,6 +42,8 @@ signal.signal(signal.SIGINT, signal.SIG_DFL) 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}") @@ -49,12 +59,15 @@ class CLI: favorites_parser.add_argument("-a", "--add-preset", metavar="PRESET_NAME", help="add preset to favorites") favorites_parser.add_argument("-r", "--remove-preset", metavar="PRESET_NAME", help="remove preset from favorites") + import_parser = subparsers.add_parser("import", help="import an 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 an preset") apply_group = apply_parser.add_mutually_exclusive_group(required=True) apply_group.add_argument("-n", "--preset-name", help="preset's display name") 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)") + #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="") @@ -78,8 +91,8 @@ class CLI: 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", action="store_true", help="enable overrides for Flatpak theming") - overrides_group.add_argument("-d", "--disable-theming", action="store_true", help="disable overrides for Flatpak theming") + overrides_group.add_argument("-e", "--enable-theming", choices=["gtk4", "gtk3", "both"], default="gtk4", help="enable overrides for Flatpak theming") + overrides_group.add_argument("-d", "--disable-theming", choices=["gtk4", "gtk3", "both"], default="gtk4", help="disable overrides for Flatpak theming") self.__parse_args() @@ -101,6 +114,9 @@ class CLI: elif args.command == "favorites": self.favorite_presets(args) + elif args.command == "import": + self.import_preset(args) + elif args.command == "apply": self.apply_preset(args) @@ -122,13 +138,26 @@ class CLI: def favorite_presets(self, args): pass + def import_preset(self, args): + pass + def apply_preset(self, args): _preset_name = args.preset_name _preset_path = args.preset_path _gtk = args.gtk - _flatpak = args.flatpak + #_flatpak = args.flatpak - print(_preset_name) + if _preset_name: + sys.stdout.write("Preset name option not implemented yet\n") + exit(1) + elif _preset_path: + preset = Preset().new_from_path(_preset_path) + + if _gtk == "gtk4": + PresetUtils().apply_preset("gtk4", preset) + elif _gtk == "gtk3": + PresetUtils().apply_preset("gtk3", preset) + sys.stdout.write("Note: In order for changes to take full effect, you need to log out.\n") def new_preset(self, args): #_interactive = args.interactive @@ -143,6 +172,16 @@ class CLI: _preset_name = args.preset_name #_custom_url = args.custom_url + for repo_name, repo in preset_repos.items(): + explore_presets, urls = fetch_presets(repo) + if explore_presets: + for (preset, preset_name), preset_url in zip(explore_presets.items(), urls): + if _preset_name.lower() in preset_name.lower(): + print(preset_name, preset_url) + else: + sys.stdout.write("Error: A transaction between server and recepient couldn't been finished.") + exit(1) + def generate_monet(self, args): _preset_name = args.preset_name _image_path = args.image_path @@ -154,17 +193,36 @@ class CLI: props = [_tone, _theme] if _preset_stdout: - preset = PresetUtils().new_preset_from_monet(name=_preset_name, monet_palette=palette, props=props, obj_only=True) + preset = PresetUtils().new_preset_from_monet(name=_preset_name, monet_palette=palette, + props=props, obj_only=True) preset_json = preset.get_preset_json() + sys.stdout.write(f"{preset_json}\n") + exit(0) - return print(preset_json) - - monet_preset = PresetUtils().new_preset_from_monet(_preset_name, palette, props) + PresetUtils().new_preset_from_monet(_preset_name, palette, props) + sys.stdout.write("Note: In order for changes to take full effect, you need to log out.\n") + # FIXME: Doesn't work in local build (settings variable doesn't have access to local settings schema) 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") + elif _enable_theming == "gtk3": + create_gtk_user_override(self.settings, "gtk3") + elif _enable_theming == "both": + create_gtk_user_override(self.settings, "gtk4") + create_gtk_user_override(self.settings, "gtk3") + + if _disable_theming == "gtk4": + remove_gtk_user_override(self.settings, "gtk4") + elif _disable_theming == "gtk3": + remove_gtk_user_override(self.settings, "gtk3") + elif _disable_theming == "both": + remove_gtk_user_override(self.settings, "gtk4") + remove_gtk_user_override(self.settings, "gtk3") + if __name__ == "__main__": cli = CLI() From 7f8e3ccc802eb7f851617e33e83608076fd7b6d4 Mon Sep 17 00:00:00 2001 From: tfuxu <73042332+tfuxu@users.noreply.github.com> Date: Sun, 11 Dec 2022 17:51:33 +0100 Subject: [PATCH 10/24] backend: convert preset_downloader module to class * add more exception handling to backend/preset_downloader and backend/models/preset modules --- gradience/backend/models/preset.py | 26 ++-- gradience/backend/preset_downloader.py | 122 +++++++++--------- gradience/frontend/cli/cli.in | 4 +- .../frontend/views/presets_manager_window.py | 4 +- .../frontend/widgets/explore_preset_row.py | 6 +- 5 files changed, 86 insertions(+), 76 deletions(-) diff --git a/gradience/backend/models/preset.py b/gradience/backend/models/preset.py index 3736ec5b..5cc45dfa 100644 --- a/gradience/backend/models/preset.py +++ b/gradience/backend/models/preset.py @@ -219,20 +219,28 @@ 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: plugins_list = plugins_list.save() - with open(self.preset_path, "w", encoding="utf-8") as file: - content = self.get_preset_json(indent=4) - file.write(content) - 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): diff --git a/gradience/backend/preset_downloader.py b/gradience/backend/preset_downloader.py index 8f697adb..b0826adb 100644 --- a/gradience/backend/preset_downloader.py +++ b/gradience/backend/preset_downloader.py @@ -29,74 +29,76 @@ from gradience.backend.logger import Logger logging = Logger() -# Open Soup3 session -session = Soup.Session() +class PresetDownloader: + def __init__(self): + self.session = Soup.Session() # Open Soup3 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}") + def fetch_presets(self, repo) -> [dict, list] or [bool, bool]: + try: + request = Soup.Message.new("GET", repo) + body = self.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 while decoding JSON data. 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 - 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: # offline + if e.code == 1: + 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/frontend/cli/cli.in b/gradience/frontend/cli/cli.in index 83eb0ac5..3c13c242 100755 --- a/gradience/frontend/cli/cli.in +++ b/gradience/frontend/cli/cli.in @@ -32,7 +32,7 @@ from gradience.backend.globals import preset_repos 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 fetch_presets, download_preset +from gradience.backend.preset_downloader import PresetDownloader from gradience.backend.flatpak_overrides import create_gtk_user_override, remove_gtk_user_override @@ -173,7 +173,7 @@ class CLI: #_custom_url = args.custom_url for repo_name, repo in preset_repos.items(): - explore_presets, urls = fetch_presets(repo) + explore_presets, urls = PresetDownloader().fetch_presets(repo=repo) if explore_presets: for (preset, preset_name), preset_url in zip(explore_presets.items(), urls): if _preset_name.lower() in preset_name.lower(): diff --git a/gradience/frontend/views/presets_manager_window.py b/gradience/frontend/views/presets_manager_window.py index 7eb27e17..9f8fd288 100644 --- a/gradience/frontend/views/presets_manager_window.py +++ b/gradience/frontend/views/presets_manager_window.py @@ -24,7 +24,7 @@ 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.preset_downloader import PresetDownloader from gradience.backend.globals import presets_dir, preset_repos from gradience.backend.constants import rootdir @@ -141,7 +141,7 @@ class GradiencePresetWindow(Adw.Window): else: badge = "white" - explore_presets, urls = fetch_presets(repo) + explore_presets, urls = PresetDownloader().fetch_presets(repo) if explore_presets: self.search_spinner.props.visible = 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")) From b36669b40e664a6db43e2c0e56262077e6e62aab Mon Sep 17 00:00:00 2001 From: tfuxu <73042332+tfuxu@users.noreply.github.com> Date: Sun, 11 Dec 2022 18:42:46 +0100 Subject: [PATCH 11/24] frontend/cli: add logic to import command, and finish download command --- gradience/frontend/cli/cli.in | 41 +++++++++++++++---- .../frontend/views/presets_manager_window.py | 1 + 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/gradience/frontend/cli/cli.in b/gradience/frontend/cli/cli.in index 3c13c242..9a017f87 100755 --- a/gradience/frontend/cli/cli.in +++ b/gradience/frontend/cli/cli.in @@ -18,16 +18,19 @@ # 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 warnings.filterwarnings("ignore") # suppress GTK warnings -from gi.repository import Gio +from gi.repository import GLib, Gio -from gradience.backend.globals import preset_repos +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 @@ -139,7 +142,24 @@ class CLI: pass def import_preset(self, args): - pass + _preset_path = args.preset_path + + preset_file = GLib.path_get_basename(_preset_path) + sys.stdout.write(f"Importing preset: {preset_file.strip()}\n") + + # TODO: Check if preset is already imported + if _preset_path.endswith(".json"): + shutil.copy( + _preset_path, + os.path.join( + presets_dir, + "user", + preset_file.strip() + ) + ) + else: + sys.stdout.write("Error: Unsupported file format, must be .json\n") + exit(1) def apply_preset(self, args): _preset_name = args.preset_name @@ -148,7 +168,7 @@ class CLI: #_flatpak = args.flatpak if _preset_name: - sys.stdout.write("Preset name option not implemented yet\n") + sys.stdout.write("Error: Preset name option not implemented yet\n") exit(1) elif _preset_path: preset = Preset().new_from_path(_preset_path) @@ -173,13 +193,18 @@ class CLI: #_custom_url = args.custom_url for repo_name, repo in preset_repos.items(): - explore_presets, urls = PresetDownloader().fetch_presets(repo=repo) + explore_presets, urls = PresetDownloader().fetch_presets(repo) if explore_presets: for (preset, preset_name), preset_url in zip(explore_presets.items(), urls): if _preset_name.lower() in preset_name.lower(): - print(preset_name, preset_url) + sys.stdout.write(f"Downloading preset: {preset_name}\n") + try: + PresetDownloader().download_preset(preset_name, to_slug_case(repo_name), preset_url) + except (GLib.GError, json.JSONDecodeError, OSError) as e: + sys.stdout.write(f"Error: An exception occurred while downloading a preset. Exc: {e}\n") + exit(1) else: - sys.stdout.write("Error: A transaction between server and recepient couldn't been finished.") + sys.stdout.write(f"Error: An error occurred while trying to fetch presets from repository. Exc: {e}\n") exit(1) def generate_monet(self, args): @@ -202,7 +227,7 @@ class CLI: PresetUtils().new_preset_from_monet(_preset_name, palette, props) sys.stdout.write("Note: In order for changes to take full effect, you need to log out.\n") - # FIXME: Doesn't work in local build (settings variable doesn't have access to local settings schema) + # FIXME: Doesn't work in local builds (settings variable doesn't have access to local settings schema) def flatpak_theming(self, args): _enable_theming = args.enable_theming _disable_theming = args.disable_theming diff --git a/gradience/frontend/views/presets_manager_window.py b/gradience/frontend/views/presets_manager_window.py index 9f8fd288..517afd00 100644 --- a/gradience/frontend/views/presets_manager_window.py +++ b/gradience/frontend/views/presets_manager_window.py @@ -310,6 +310,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 From 3c850380be4a0db4237ffd435c8c6786c587c472 Mon Sep 17 00:00:00 2001 From: tfuxu <73042332+tfuxu@users.noreply.github.com> Date: Tue, 13 Dec 2022 19:27:20 +0100 Subject: [PATCH 12/24] frontend/cli: change some help messages --- gradience/frontend/cli/cli.in | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/gradience/frontend/cli/cli.in b/gradience/frontend/cli/cli.in index 9a017f87..768f8fd7 100755 --- a/gradience/frontend/cli/cli.in +++ b/gradience/frontend/cli/cli.in @@ -57,45 +57,46 @@ class CLI: #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") - favorites_parser = subparsers.add_parser("favorites", help="list favorited presets") - favorites_parser.add_argument("-a", "--add-preset", metavar="PRESET_NAME", help="add preset to favorites") - favorites_parser.add_argument("-r", "--remove-preset", metavar="PRESET_NAME", help="remove preset from favorites") + 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") - import_parser = subparsers.add_parser("import", help="import an preset") + 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 an preset") + 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="preset's display name") + 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="preset's display name", required=True) + 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("--preset-stdout", action="store_true", help="print out preset in JSON format directly to stdout") - download_parser = subparsers.add_parser("download", help="download preset from internet") + download_parser = subparsers.add_parser("download", help="download preset from preset repository") #new_parser.add_argument("-i", "--interactive", action="store_true", help="") - download_parser.add_argument("-n", "--preset-name", help="", required=True) + 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 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="set a tone for colors (default: 20)") + 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("--preset-stdout", action="store_true", help="print out preset in JSON format directly to stdout") 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"], default="gtk4", help="enable overrides for Flatpak theming") - overrides_group.add_argument("-d", "--disable-theming", choices=["gtk4", "gtk3", "both"], default="gtk4", help="disable overrides for Flatpak theming") + 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() @@ -201,10 +202,10 @@ class CLI: try: PresetDownloader().download_preset(preset_name, to_slug_case(repo_name), preset_url) except (GLib.GError, json.JSONDecodeError, OSError) as e: - sys.stdout.write(f"Error: An exception occurred while downloading a preset. Exc: {e}\n") + sys.stdout.write(f"Error: An error occurred while downloading a preset. Exc: {e}\n") exit(1) else: - sys.stdout.write(f"Error: An error occurred while trying to fetch presets from repository. Exc: {e}\n") + sys.stdout.write(f"Error: An error occurred while trying to fetch presets from repository.\n") exit(1) def generate_monet(self, args): From da0300e934d7f256c570b47e9c582e6aaa8c1ba4 Mon Sep 17 00:00:00 2001 From: tfuxu <73042332+tfuxu@users.noreply.github.com> Date: Sat, 17 Dec 2022 17:24:16 +0100 Subject: [PATCH 13/24] frontend/cli: use from now backend/logger module functions to print messages in CLI --- gradience/frontend/cli/cli.in | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/gradience/frontend/cli/cli.in b/gradience/frontend/cli/cli.in index 768f8fd7..271a3b34 100755 --- a/gradience/frontend/cli/cli.in +++ b/gradience/frontend/cli/cli.in @@ -38,6 +38,10 @@ from gradience.backend.theming.preset_utils import PresetUtils from gradience.backend.preset_downloader import PresetDownloader from gradience.backend.flatpak_overrides import create_gtk_user_override, remove_gtk_user_override +from gradience.backend.logger import Logger + +logging = Logger() + version = "@VERSION@" @@ -58,10 +62,12 @@ class CLI: 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", 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) @@ -79,7 +85,7 @@ class CLI: 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("--preset-stdout", action="store_true", help="print out preset in JSON format directly to stdout") + 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 preset repository") #new_parser.add_argument("-i", "--interactive", action="store_true", help="") @@ -91,7 +97,7 @@ class CLI: 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("--preset-stdout", action="store_true", help="print out preset in JSON format directly to stdout") + monet_parser.add_argument("-j", "--json", action="store_true", help="print out a result of this command directly in JSON format") overrides_parser = subparsers.add_parser("flatpak-overrides", help="enable or disable Flatpak theming") overrides_group = overrides_parser.add_mutually_exclusive_group(required=True) @@ -146,7 +152,7 @@ class CLI: _preset_path = args.preset_path preset_file = GLib.path_get_basename(_preset_path) - sys.stdout.write(f"Importing preset: {preset_file.strip()}\n") + logging.info(f"Importing preset: {preset_file.strip()}") # TODO: Check if preset is already imported if _preset_path.endswith(".json"): @@ -159,7 +165,7 @@ class CLI: ) ) else: - sys.stdout.write("Error: Unsupported file format, must be .json\n") + logging.error("Unsupported file format, must be .json") exit(1) def apply_preset(self, args): @@ -169,7 +175,7 @@ class CLI: #_flatpak = args.flatpak if _preset_name: - sys.stdout.write("Error: Preset name option not implemented yet\n") + logging.error("Preset name option not implemented yet") exit(1) elif _preset_path: preset = Preset().new_from_path(_preset_path) @@ -178,7 +184,7 @@ class CLI: PresetUtils().apply_preset("gtk4", preset) elif _gtk == "gtk3": PresetUtils().apply_preset("gtk3", preset) - sys.stdout.write("Note: In order for changes to take full effect, you need to log out.\n") + logging.info("In order for changes to take full effect, you need to log out.") def new_preset(self, args): #_interactive = args.interactive @@ -186,7 +192,7 @@ class CLI: _colors = args.colors _palette = args.palette _custom_css = args.custom_css - _preset_stdout = args.preset_stdout + _json = args.json def download_preset(self, args): #_interactive = args.interactive @@ -198,11 +204,11 @@ class CLI: if explore_presets: for (preset, preset_name), preset_url in zip(explore_presets.items(), urls): if _preset_name.lower() in preset_name.lower(): - sys.stdout.write(f"Downloading preset: {preset_name}\n") + 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: - sys.stdout.write(f"Error: An error occurred while downloading a preset. Exc: {e}\n") + logging.error(f"An error occurred while downloading a preset. Exc: {e}") exit(1) else: sys.stdout.write(f"Error: An error occurred while trying to fetch presets from repository.\n") @@ -213,20 +219,21 @@ class CLI: _image_path = args.image_path _tone = args.tone _theme = args.theme - _preset_stdout = args.preset_stdout + _json = args.json palette = Monet().generate_from_image(_image_path) props = [_tone, _theme] - if _preset_stdout: + 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() - sys.stdout.write(f"{preset_json}\n") + logging.info("Generated monet preset:") + print(preset_json) exit(0) PresetUtils().new_preset_from_monet(_preset_name, palette, props) - sys.stdout.write("Note: In order for changes to take full effect, you need to log out.\n") + logging.info("In order for changes to take full effect, you need to log out.") # FIXME: Doesn't work in local builds (settings variable doesn't have access to local settings schema) def flatpak_theming(self, args): From d3df5c37da481aa3b8b95fdbcf218bb9b657f7f0 Mon Sep 17 00:00:00 2001 From: tfuxu <73042332+tfuxu@users.noreply.github.com> Date: Sat, 17 Dec 2022 18:42:29 +0100 Subject: [PATCH 14/24] backend: change the structure of log messages, and move preset listing function to backend * add ability to name modules in logger object, * slightly clean PresetDownloader class and return exceptions on error encounters --- gradience/backend/logger.py | 36 +++++++---- gradience/backend/preset_downloader.py | 19 +++--- gradience/backend/theming/preset_utils.py | 64 ++++++++++++++++++- .../frontend/views/presets_manager_window.py | 20 ++++-- 4 files changed, 111 insertions(+), 28 deletions(-) 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/preset_downloader.py b/gradience/backend/preset_downloader.py index b0826adb..c5c2557a 100644 --- a/gradience/backend/preset_downloader.py +++ b/gradience/backend/preset_downloader.py @@ -31,24 +31,25 @@ logging = Logger() class PresetDownloader: def __init__(self): - self.session = Soup.Session() # Open Soup3 session + # Open Soup3 session + self.session = Soup.Session() - def fetch_presets(self, repo) -> [dict, list] or [bool, bool]: + 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: # offline - if e.code == 1: + except GLib.GError as e: + if e.code == 1: # offline logging.error(f"Failed to establish a new connection. Exc: {e}") - return False, False + raise else: logging.error(f"Unhandled Libsoup3 GLib.GError error code {e.code}. Exc: {e}") - return False, False + raise try: raw = json.loads(body.get_data()) except json.JSONDecodeError as e: logging.error(f"Error while decoding JSON data. Exc: {e}") - return False, False + raise preset_dict = {} url_list = [] @@ -72,8 +73,8 @@ class PresetDownloader: try: request = Soup.Message.new("GET", repo) body = self.session.send_and_read(request, None) - except GLib.GError as e: # offline - if e.code == 1: + except GLib.GError as e: + if e.code == 1: # offline logging.error(f"Failed to establish a new connection. Exc: {e}") raise else: diff --git a/gradience/backend/theming/preset_utils.py b/gradience/backend/theming/preset_utils.py index 89bbd589..aeb01541 100644 --- a/gradience/backend/theming/preset_utils.py +++ b/gradience/backend/theming/preset_utils.py @@ -18,6 +18,7 @@ import os import json +from pathlib import Path from gi.repository import GLib, Gio @@ -25,7 +26,7 @@ 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 get_gtk_theme_dir +from gradience.backend.globals import presets_dir, get_gtk_theme_dir from gradience.backend.logger import Logger @@ -184,6 +185,67 @@ class PresetUtils: 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) diff --git a/gradience/frontend/views/presets_manager_window.py b/gradience/frontend/views/presets_manager_window.py index 517afd00..e365fbc0 100644 --- a/gradience/frontend/views/presets_manager_window.py +++ b/gradience/frontend/views/presets_manager_window.py @@ -141,9 +141,19 @@ class GradiencePresetWindow(Adw.Window): else: badge = "white" - explore_presets, urls = PresetDownloader().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( @@ -154,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": From 50205619fa575b2fbbacc18912aca57c125b25f1 Mon Sep 17 00:00:00 2001 From: tfuxu <73042332+tfuxu@users.noreply.github.com> Date: Sat, 17 Dec 2022 18:58:30 +0100 Subject: [PATCH 15/24] frontend/cli: fix local builds support, add logic code for all remaining commands * add logic code for `presets` and `favorites` command, * disable `new` command for now (I need to think later how users should input required values in this command), * add `No presets found` statement in `download` command, * create new local_cli.sh script for easier CLI testing on local builds --- gradience/frontend/cli/cli.in | 118 ++++++++++++++++++++++++----- gradience/frontend/cli/meson.build | 8 ++ local_cli.sh | 21 +++++ 3 files changed, 126 insertions(+), 21 deletions(-) create mode 100755 local_cli.sh diff --git a/gradience/frontend/cli/cli.in b/gradience/frontend/cli/cli.in index 271a3b34..9c910d7d 100755 --- a/gradience/frontend/cli/cli.in +++ b/gradience/frontend/cli/cli.in @@ -26,7 +26,20 @@ 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 @@ -43,17 +56,13 @@ from gradience.backend.logger import Logger logging = Logger() -version = "@VERSION@" - -signal.signal(signal.SIGINT, signal.SIG_DFL) - - 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") @@ -61,8 +70,8 @@ class CLI: #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", help="print out a result of this command directly in JSON format") + #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") @@ -79,13 +88,13 @@ class CLI: 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 = 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") + #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 preset repository") #new_parser.add_argument("-i", "--interactive", action="store_true", help="") @@ -143,10 +152,68 @@ class CLI: self.flatpak_theming(args) def list_presets(self, args): - pass + #_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): - pass + _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()) + + print(presets_name) + print(favorite) + + 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)) + 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)) + 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 @@ -194,15 +261,25 @@ class CLI: _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 for repo_name, repo in preset_repos.items(): - explore_presets, urls = PresetDownloader().fetch_presets(repo) - if explore_presets: + 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: 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: @@ -210,9 +287,9 @@ class CLI: except (GLib.GError, json.JSONDecodeError, OSError) as e: logging.error(f"An error occurred while downloading a preset. Exc: {e}") exit(1) - else: - sys.stdout.write(f"Error: An error occurred while trying to fetch presets from repository.\n") - exit(1) + else: + logging.error(f"No presets found with text: {_preset_name}") + exit(1) def generate_monet(self, args): _preset_name = args.preset_name @@ -228,7 +305,6 @@ class CLI: preset = PresetUtils().new_preset_from_monet(name=_preset_name, monet_palette=palette, props=props, obj_only=True) preset_json = preset.get_preset_json() - logging.info("Generated monet preset:") print(preset_json) exit(0) diff --git a/gradience/frontend/cli/meson.build b/gradience/frontend/cli/meson.build index a5ee8000..84c61f35 100644 --- a/gradience/frontend/cli/meson.build +++ b/gradience/frontend/cli/meson.build @@ -8,6 +8,14 @@ configure_file( 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' ] diff --git a/local_cli.sh b/local_cli.sh new file mode 100755 index 00000000..181e1b92 --- /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 $@ From 20994dfdf111ee1a34ce7ec899dfca178a5dbaa2 Mon Sep 17 00:00:00 2001 From: tfuxu <73042332+tfuxu@users.noreply.github.com> Date: Sun, 18 Dec 2022 15:46:15 +0100 Subject: [PATCH 16/24] frontend/cli: add information for Flatpak users about `monet` command This commit adds error message that shows up when `monet` command is executed on Flatpak builds. --- gradience/frontend/cli/cli.in | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/gradience/frontend/cli/cli.in b/gradience/frontend/cli/cli.in index 9c910d7d..bce49658 100755 --- a/gradience/frontend/cli/cli.in +++ b/gradience/frontend/cli/cli.in @@ -40,7 +40,7 @@ signal.signal(signal.SIGINT, signal.SIG_DFL) warnings.filterwarnings("ignore") # suppress GTK warnings -from gi.repository import GLib, Gio +from gi.repository import GLib, Gio, Xdp from gradience.backend.utils.common import to_slug_case from gradience.backend.globals import preset_repos, presets_dir @@ -58,6 +58,7 @@ logging = Logger() class CLI: settings = Gio.Settings.new("@APP_ID@") + portal = Xdp.Portal() def __init__(self): self.parser = argparse.ArgumentParser(description="Gradience - change the look of Adwaita, with ease") @@ -176,9 +177,6 @@ class CLI: presets_list = PresetUtils().get_presets_list() presets_name = list(presets_list.values()) - print(presets_name) - print(favorite) - if _json and not _add_preset and not _remove_preset: favorites_json = {"favorites": list(favorite), "amount": len(favorite)} json_output = json.dumps(favorites_json) @@ -195,7 +193,7 @@ class CLI: 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.") + "Check if you typed the correct preset name, or try importing your preset using 'import' command.") exit(1) if _remove_preset: @@ -291,6 +289,10 @@ class CLI: logging.error(f"No presets found with text: {_preset_name}") exit(1) + # TODO: Fix support for Flatpak builds \ + # Current issue: Monet class can't generate Monet palette from image located in host, because it doesn't have any permissions to read user directories. \ + # It is recommended to use portals instead of just allowing Gradience to read all user files. For this purpose, we can should be able to use one of the available portals. \ + # Possible useful portals: 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 @@ -298,6 +300,11 @@ class CLI: _theme = args.theme _json = args.json + is_sandboxed = self.portal.running_under_sandbox() + if is_sandboxed: + logging.error("Preset generation in 'monet' command isn't yet available for Flatpak installations.") + exit(1) + palette = Monet().generate_from_image(_image_path) props = [_tone, _theme] @@ -311,7 +318,6 @@ class CLI: PresetUtils().new_preset_from_monet(_preset_name, palette, props) logging.info("In order for changes to take full effect, you need to log out.") - # FIXME: Doesn't work in local builds (settings variable doesn't have access to local settings schema) def flatpak_theming(self, args): _enable_theming = args.enable_theming _disable_theming = args.disable_theming From 6872919505e77f4100b3c5204670e00810a9e667 Mon Sep 17 00:00:00 2001 From: tfuxu <73042332+tfuxu@users.noreply.github.com> Date: Sun, 18 Dec 2022 15:55:13 +0100 Subject: [PATCH 17/24] fix: allow spaces in argument values when executing CLI from `local_cli.sh` --- local_cli.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/local_cli.sh b/local_cli.sh index 181e1b92..78803dc3 100755 --- a/local_cli.sh +++ b/local_cli.sh @@ -18,4 +18,4 @@ # 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 $@ +python builddir/gradience/frontend/gradience-cli "$@" From db7347cf18708353efdc57a651cc49b5a49222d9 Mon Sep 17 00:00:00 2001 From: tfuxu <73042332+tfuxu@users.noreply.github.com> Date: Sun, 18 Dec 2022 16:36:25 +0100 Subject: [PATCH 18/24] frontend/cli: add more messages in CLI to make it more user-friendly * finish `apply` command * remove `frontend/settings_schema` module from imports in backend modules --- gradience/backend/models/preset.py | 8 ++++++-- gradience/frontend/cli/cli.in | 22 ++++++++++++++++++++-- gradience/frontend/views/main_window.py | 4 ++-- 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/gradience/backend/models/preset.py b/gradience/backend/models/preset.py index 5cc45dfa..95f8822b 100644 --- a/gradience/backend/models/preset.py +++ b/gradience/backend/models/preset.py @@ -19,7 +19,6 @@ 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 @@ -95,6 +94,11 @@ adw_palette = { } } +custom_css_app_types = [ + "gtk4", + "gtk3" +] + class Preset: variables = {} @@ -174,7 +178,7 @@ 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: logging.error(f"Failed to create a new preset object. Exc: {e}") diff --git a/gradience/frontend/cli/cli.in b/gradience/frontend/cli/cli.in index bce49658..bc2ac342 100755 --- a/gradience/frontend/cli/cli.in +++ b/gradience/frontend/cli/cli.in @@ -190,6 +190,7 @@ class CLI: 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. " @@ -200,6 +201,7 @@ class CLI: 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. " @@ -229,6 +231,7 @@ class CLI: preset_file.strip() ) ) + logging.info("Preset imported successfully.") else: logging.error("Unsupported file format, must be .json") exit(1) @@ -239,16 +242,23 @@ class CLI: _gtk = args.gtk #_flatpak = args.flatpak + presets_list = PresetUtils().get_presets_list() + presets_name = list(presets_list.values()) + if _preset_name: - logging.error("Preset name option not implemented yet") - exit(1) + 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): @@ -285,6 +295,8 @@ class CLI: 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.") else: logging.error(f"No presets found with text: {_preset_name}") exit(1) @@ -324,19 +336,25 @@ class CLI: 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__": diff --git a/gradience/frontend/views/main_window.py b/gradience/frontend/views/main_window.py index 4151b196..20bcc552 100644 --- a/gradience/frontend/views/main_window.py +++ b/gradience/frontend/views/main_window.py @@ -75,8 +75,8 @@ class GradienceMainWindow(Adw.ApplicationWindow): 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(): From 1e8de798dbd935ca39fb65645ba64124f7c0284a Mon Sep 17 00:00:00 2001 From: David Lapshin Date: Sun, 18 Dec 2022 19:27:51 +0300 Subject: [PATCH 19/24] fix: text --- gradience/frontend/cli/cli.in | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gradience/frontend/cli/cli.in b/gradience/frontend/cli/cli.in index bc2ac342..21974757 100755 --- a/gradience/frontend/cli/cli.in +++ b/gradience/frontend/cli/cli.in @@ -61,7 +61,7 @@ class CLI: portal = Xdp.Portal() def __init__(self): - self.parser = argparse.ArgumentParser(description="Gradience - change the look of Adwaita, with ease") + 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') @@ -97,12 +97,12 @@ class CLI: #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 preset repository") + 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 image") + 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)") From 354010ae2203c759c1f3dc5a879e89a773fa6e0c Mon Sep 17 00:00:00 2001 From: tfuxu <73042332+tfuxu@users.noreply.github.com> Date: Sun, 18 Dec 2022 18:59:43 +0100 Subject: [PATCH 20/24] frontend/cli: fix `download` command failing after indexing first preset --- gradience/frontend/cli/cli.in | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/gradience/frontend/cli/cli.in b/gradience/frontend/cli/cli.in index 21974757..6687a773 100755 --- a/gradience/frontend/cli/cli.in +++ b/gradience/frontend/cli/cli.in @@ -279,6 +279,8 @@ class CLI: _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) @@ -286,6 +288,8 @@ class CLI: 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(): @@ -297,9 +301,14 @@ class CLI: exit(1) else: logging.info("Preset downloaded successfully.") + exit(0) else: - logging.error(f"No presets found with text: {_preset_name}") - exit(1) + 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 # TODO: Fix support for Flatpak builds \ # Current issue: Monet class can't generate Monet palette from image located in host, because it doesn't have any permissions to read user directories. \ From c9d101da0bcb55dc756a1c56fe7537dd8a9129f5 Mon Sep 17 00:00:00 2001 From: tfuxu <73042332+tfuxu@users.noreply.github.com> Date: Mon, 19 Dec 2022 18:31:45 +0100 Subject: [PATCH 21/24] backend/flatpak_overrides: add new functions for file access overrides --- gradience/backend/flatpak_overrides.py | 205 +++++++++++++++++++++---- 1 file changed, 176 insertions(+), 29 deletions(-) diff --git a/gradience/backend/flatpak_overrides.py b/gradience/backend/flatpak_overrides.py index e57674a8..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,7 +62,7 @@ def get_user_flatpak_path(): return GLib.build_filenamev([userDataDir, "flatpak"]) -def user_save_keyfile(settings, user_keyfile, filename, gtk_ver, toast_overlay=None): +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: @@ -68,19 +70,21 @@ def user_save_keyfile(settings, user_keyfile, filename, gtk_ver, toast_overlay=N 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(settings, global_keyfile, filename, gtk_ver, toast_overlay=None): +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: @@ -88,23 +92,166 @@ def global_save_keyfile(settings, global_keyfile, filename, gtk_ver, toast_overl 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 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"]) + override_dir = GLib.build_filenamev([__get_user_flatpak_path(), "overrides"]) logging.debug(f"override_dir: {override_dir}") filename = GLib.build_filenamev([override_dir, "global"]) @@ -149,8 +296,8 @@ def create_gtk_user_override(settings, gtk_ver, toast_overlay=None): user_keyfile.load_from_file(filename, GLib.KeyFileFlags.NONE) user_keyfile.set_string("Context", "filesystems", gtk_path) - user_save_keyfile(settings, user_keyfile, - filename, gtk_ver, toast_overlay) + __user_save_keyfile(user_keyfile, filename, + settings, gtk_ver, toast_overlay) else: if toast_overlay: toast_overlay.add_toast( @@ -163,15 +310,15 @@ def create_gtk_user_override(settings, gtk_ver, toast_overlay=None): "Context", "filesystems") except GLib.GError: user_keyfile.set_string("Context", "filesystems", gtk_path) - user_save_keyfile(settings, user_keyfile, - filename, gtk_ver, toast_overlay) + __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(settings, user_keyfile, - filename, gtk_ver, toast_overlay) + __user_save_keyfile(user_keyfile, filename, + settings, gtk_ver, toast_overlay) else: if is_gtk4: settings.set_boolean("user-flatpak-theming-gtk4", True) @@ -181,7 +328,7 @@ def create_gtk_user_override(settings, gtk_ver, toast_overlay=None): def remove_gtk_user_override(settings, gtk_ver, toast_overlay=None): - override_dir = GLib.build_filenamev([get_user_flatpak_path(), "overrides"]) + override_dir = GLib.build_filenamev([__get_user_flatpak_path(), "overrides"]) logging.debug(f"override_dir: {override_dir}") filename = GLib.build_filenamev([override_dir, "global"]) @@ -232,8 +379,8 @@ def remove_gtk_user_override(settings, gtk_ver, toast_overlay=None): user_keyfile.set_string_list( "Context", "filesystems", filesys_list) - user_save_keyfile(settings, user_keyfile, - filename, gtk_ver, toast_overlay) + __user_save_keyfile(user_keyfile, filename, + settings, gtk_ver, toast_overlay) logging.debug("remove override: Value removed.") else: set_theming() @@ -246,7 +393,7 @@ def remove_gtk_user_override(settings, gtk_ver, toast_overlay=None): 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"]) @@ -291,8 +438,8 @@ def create_gtk_global_override(settings, gtk_ver, toast_overlay=None): global_keyfile.load_from_file(filename, GLib.KeyFileFlags.NONE) global_keyfile.set_string("Context", "filesystems", gtk_path) - global_save_keyfile(settings, global_keyfile, - filename, gtk_ver, toast_overlay) + __global_save_keyfile(global_keyfile, filename, + settings, gtk_ver, toast_overlay) else: if toast_overlay: toast_overlay.add_toast( @@ -305,15 +452,15 @@ def create_gtk_global_override(settings, gtk_ver, toast_overlay=None): "Context", "filesystems") except GLib.GError: global_keyfile.set_string("Context", "filesystems", gtk_path) - global_save_keyfile(settings, global_keyfile, - filename, gtk_ver, toast_overlay) + __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(settings, global_keyfile, - filename, gtk_ver, toast_overlay) + __global_save_keyfile(global_keyfile, filename, + settings, gtk_ver, toast_overlay) else: if is_gtk4: settings.set_boolean("global-flatpak-theming-gtk4", True) @@ -324,7 +471,7 @@ def create_gtk_global_override(settings, gtk_ver, toast_overlay=None): 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"]) @@ -375,8 +522,8 @@ def remove_gtk_global_override(settings, gtk_ver, toast_overlay=None): global_keyfile.set_string_list( "Context", "filesystems", filesys_list) - global_save_keyfile(settings, global_keyfile, - filename, gtk_ver, toast_overlay) + __global_save_keyfile(global_keyfile, filename, + settings, gtk_ver, toast_overlay) logging.debug("remove override: Value removed.") else: set_theming() From 03a63e7a33e96de3c4fdf14fb2099aa4525386e0 Mon Sep 17 00:00:00 2001 From: tfuxu <73042332+tfuxu@users.noreply.github.com> Date: Mon, 19 Dec 2022 20:17:35 +0100 Subject: [PATCH 22/24] frontend/cli: add new `access-file` command * add try..except statement to shutil.copy operation in `import` command --- gradience/frontend/cli/cli.in | 73 ++++++++++++++++++++++++++++++----- 1 file changed, 64 insertions(+), 9 deletions(-) diff --git a/gradience/frontend/cli/cli.in b/gradience/frontend/cli/cli.in index 6687a773..7c7c78ab 100755 --- a/gradience/frontend/cli/cli.in +++ b/gradience/frontend/cli/cli.in @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# cli.py +# cli.in # # Change the look of Adwaita, with ease # Copyright (C) 2022 Gradience Team @@ -49,7 +49,7 @@ 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 create_gtk_user_override, remove_gtk_user_override +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 @@ -109,6 +109,12 @@ class CLI: 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") @@ -149,6 +155,9 @@ class CLI: 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) @@ -223,14 +232,18 @@ class CLI: # TODO: Check if preset is already imported if _preset_path.endswith(".json"): - shutil.copy( - _preset_path, - os.path.join( - presets_dir, - "user", - preset_file.strip() + 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") @@ -339,6 +352,48 @@ class CLI: 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 allows 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 From ca84565745db75f7fd235175da374979151f8aa2 Mon Sep 17 00:00:00 2001 From: tfuxu <73042332+tfuxu@users.noreply.github.com> Date: Mon, 19 Dec 2022 20:19:32 +0100 Subject: [PATCH 23/24] build-aux/flatpak: add `xdg-download` read-only as a allowed directory --- build-aux/flatpak/com.github.GradienceTeam.Gradience.Devel.json | 1 + build-aux/flatpak/com.github.GradienceTeam.Gradience.json | 1 + 2 files changed, 2 insertions(+) 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", From 61eb5fb8655752217915055a73b3bb9b7e94d6cb Mon Sep 17 00:00:00 2001 From: tfuxu <73042332+tfuxu@users.noreply.github.com> Date: Mon, 19 Dec 2022 20:28:53 +0100 Subject: [PATCH 24/24] frontend/cli: reenable `monet` command on Flatpak builds * move sandbox check from `frontend/cli` module to `backend/globals` --- gradience/backend/globals.py | 12 ++++++++++++ gradience/frontend/cli/cli.in | 15 +++------------ 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/gradience/backend/globals.py b/gradience/backend/globals.py index 08552452..1f615b34 100644 --- a/gradience/backend/globals.py +++ b/gradience/backend/globals.py @@ -18,6 +18,8 @@ import os +from gi.repository import Xdp + presets_dir = os.path.join( os.environ.get("XDG_CONFIG_HOME", os.environ["HOME"] + "/.config"), @@ -44,3 +46,13 @@ def get_gtk_theme_dir(app_type): ) 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/frontend/cli/cli.in b/gradience/frontend/cli/cli.in index 7c7c78ab..f4171847 100755 --- a/gradience/frontend/cli/cli.in +++ b/gradience/frontend/cli/cli.in @@ -40,7 +40,7 @@ signal.signal(signal.SIGINT, signal.SIG_DFL) warnings.filterwarnings("ignore") # suppress GTK warnings -from gi.repository import GLib, Gio, Xdp +from gi.repository import GLib, Gio from gradience.backend.utils.common import to_slug_case from gradience.backend.globals import preset_repos, presets_dir @@ -58,7 +58,6 @@ logging = Logger() class CLI: settings = Gio.Settings.new("@APP_ID@") - portal = Xdp.Portal() def __init__(self): self.parser = argparse.ArgumentParser(description="Gradience - Change the look of Adwaita, with ease") @@ -323,10 +322,7 @@ class CLI: continue repo_no += 1 - # TODO: Fix support for Flatpak builds \ - # Current issue: Monet class can't generate Monet palette from image located in host, because it doesn't have any permissions to read user directories. \ - # It is recommended to use portals instead of just allowing Gradience to read all user files. For this purpose, we can should be able to use one of the available portals. \ - # Possible useful portals: org.freedesktop.portal.Documents (support missing in libportal, only D-Bus calls), org.freedesktop.portal.FileChooser + # 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 @@ -334,11 +330,6 @@ class CLI: _theme = args.theme _json = args.json - is_sandboxed = self.portal.running_under_sandbox() - if is_sandboxed: - logging.error("Preset generation in 'monet' command isn't yet available for Flatpak installations.") - exit(1) - palette = Monet().generate_from_image(_image_path) props = [_tone, _theme] @@ -371,7 +362,7 @@ class CLI: print(value) exit(0) else: - print("0 allows found") + print("0 paths found") exit(0) if _allow: