#!/usr/bin/env python3 # cli.in # # Change the look of Adwaita, with ease # Copyright (C) 2022-2023, Gradience Team # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import os import sys import json import shutil import signal import argparse import warnings version = "@VERSION@" is_local = @local_build@ if is_local: # In the local use case, use gradience module from the sourcetree sys.path.insert(1, '@PYTHON@') # In the local use case the installed schemas go in /data os.environ["XDG_DATA_DIRS"] = '@SCHEMAS_DIR@:' + os.environ.get("XDG_DATA_DIRS", "") signal.signal(signal.SIGINT, signal.SIG_DFL) warnings.filterwarnings("ignore") # suppress GTK warnings from gi.repository import GLib, Gio from gradience.backend.utils.common import to_slug_case from gradience.backend.globals import preset_repos, presets_dir from gradience.backend.theming.monet import Monet from gradience.backend.models.preset import Preset from gradience.backend.theming.preset_utils import PresetUtils from gradience.backend.preset_downloader import PresetDownloader from gradience.backend.flatpak_overrides import list_file_access, allow_file_access, disallow_file_access, create_gtk_user_override, remove_gtk_user_override from gradience.backend.logger import Logger logging = Logger() class CLI: settings = Gio.Settings.new("@APP_ID@") def __init__(self): self.parser = argparse.ArgumentParser(description="Gradience - Change the look of Adwaita, with ease") self.parser.add_argument("-V", "--version", action="version", version=f"Gradience, version {version}") #self.parser.add_argument("-j", "--json", action="store_true", help="print out a result of the command directly in JSON format") #self.parser.add_argument('-J', '--pretty-json', dest='pretty_json', action='store_true', help='pretty-print JSON output') subparsers = self.parser.add_subparsers(dest="command") #info_parser = subparsers.add_parser("info", help="show information about Gradience") presets_parser = subparsers.add_parser("presets", help="list installed presets") presets_parser.add_argument("-j", "--json", action="store_true", help="print out a result of this command directly in JSON format") favorites_parser = subparsers.add_parser("favorites", help="list favorite presets") favorites_parser.add_argument("-a", "--add-preset", metavar="PRESET_NAME", help="add a preset to favorites") favorites_parser.add_argument("-r", "--remove-preset", metavar="PRESET_NAME", help="remove a preset from favorites") favorites_parser.add_argument("-j", "--json", action="store_true", help="print out a result of this command directly in JSON format") import_parser = subparsers.add_parser("import", help="import a preset") import_parser.add_argument("-p", "--preset-path", help="absolute path to a preset file", required=True) apply_parser = subparsers.add_parser("apply", help="apply a preset") apply_group = apply_parser.add_mutually_exclusive_group(required=True) apply_group.add_argument("-n", "--preset-name", help="display name for a preset") apply_group.add_argument("-p", "--preset-path", help="absolute path to a preset file") apply_parser.add_argument("--gtk", choices=["gtk4", "gtk3", "both"], default="gtk4", help="types of applications you want to theme (default: gtk4)") #new_parser = subparsers.add_parser("new", help="create a new preset") #new_parser.add_argument("-i", "--interactive", action="store_true", help="") #new_parser.add_argument("-n", "--name", help="display name for a preset", required=True) #new_parser.add_argument("--colors", help="", required=True) #new_parser.add_argument("--palette", help="") #new_parser.add_argument("--custom-css", help="") #new_parser.add_argument("-j", "--json", action="store_true", help="print out a result of this command directly in JSON format") download_parser = subparsers.add_parser("download", help="download preset from a preset repository") #new_parser.add_argument("-i", "--interactive", action="store_true", help="") download_parser.add_argument("-n", "--preset-name", help="name of a preset you want to get", required=True) #download_parser.add_argument("--custom-url", help="use custom repository's presets.json to download other presets") monet_parser = subparsers.add_parser("monet", help="generate Material You preset from an image") #monet_parser.add_argument("-a", "--apply", help="apply Monet's generated preset after it has been created", action='store_true') monet_parser.add_argument("-n", "--preset-name", help="name for a generated preset", required=True) monet_parser.add_argument("-p", "--image-path", help="absolute path to image", required=True) monet_parser.add_argument("--tone", default=20, help="a tone for colors (default: 20)") monet_parser.add_argument("--theme", choices=["light", "dark"], default="light", help="choose whatever it should be a light or dark theme (default: light)") monet_parser.add_argument("-j", "--json", action="store_true", help="print out a result of this command directly in JSON format") access_parser = subparsers.add_parser("access-file", help="allow or disallow Gradience to access a certain file or directory") access_parser.add_argument("-l", "--list", action="store_true", help="list allowed directories and files") access_group = access_parser.add_mutually_exclusive_group(required=False) access_group.add_argument("-a", "--allow", metavar="PATH", help="allow Gradience access to this file or directory") access_group.add_argument("-d", "--disallow", metavar="PATH", help="disallow Gradience access to this file or directory") overrides_parser = subparsers.add_parser("flatpak-overrides", help="enable or disable Flatpak theming") overrides_group = overrides_parser.add_mutually_exclusive_group(required=True) overrides_group.add_argument("-e", "--enable-theming", choices=["gtk4", "gtk3", "both"], help="enable overrides for Flatpak theming") overrides_group.add_argument("-d", "--disable-theming", choices=["gtk4", "gtk3", "both"], help="disable overrides for Flatpak theming") self.__parse_args() def __print_json(self, data, pretty=False): if pretty: print(json.dumps(data, indent=4)) else: print(json.dumps(data)) def __parse_args(self): args = self.parser.parse_args() if not args.command: print(self.parser.format_help()) if args.command == "presets": self.list_presets(args) elif args.command == "favorites": self.favorite_presets(args) elif args.command == "import": self.import_preset(args) elif args.command == "apply": self.apply_preset(args) elif args.command == "new": self.new_preset(args) elif args.command == "download": self.download_preset(args) elif args.command == "monet": self.generate_monet(args) elif args.command == "access-file": self.access_file(args) elif args.command == "flatpak-overrides": self.flatpak_theming(args) def list_presets(self, args): #_remove_preset = args.remove_preset _json = args.json try: presets_list = PresetUtils().get_presets_list(full_list=True) except (OSError, KeyError, AttributeError) as e: logging.error("Failed to retrieve a list of presets.", exc=e) exit(1) if _json: json_output = json.dumps(presets_list) print(json_output) exit(0) # TODO: Modify this output to look more like a table (maybe use ncurses?) print("\033[1;37mPreset name\033[0m | \033[1;37mPreset path\033[0m") for key in presets_list: print(f"{presets_list[key]} -> {key}") def favorite_presets(self, args): _add_preset = args.add_preset _remove_preset = args.remove_preset _json = args.json favorite = set(self.settings.get_value("favourite")) try: presets_list = PresetUtils().get_presets_list(full_list=True) except (OSError, KeyError, AttributeError) as e: logging.error("Failed to retrieve a list of presets.", exc=e) exit(1) presets_name = list(presets_list.values()) if _json and not _add_preset and not _remove_preset: favorites_json = {"favorites": list(favorite), "amount": len(favorite)} json_output = json.dumps(favorites_json) print(json_output) exit(0) elif _json and _add_preset or _json and _remove_preset: logging.error("JSON output option isn't available for --add-preset and --remove-preset options.") exit(1) if _add_preset: if _add_preset in presets_name: favorite.add(_add_preset) self.settings.set_value("favourite", GLib.Variant("as", favorite)) logging.info(f"Preset {_add_preset} has been added to favorites.") exit(0) else: logging.error(f"Preset named {_add_preset} isn't installed in Gradience. " "Check if you typed the correct preset name, or try importing your preset using 'import' command.") exit(1) if _remove_preset: if _remove_preset in favorite: favorite.remove(_remove_preset) self.settings.set_value("favourite", GLib.Variant("as", favorite)) logging.info(f"Preset {_add_preset} has been removed from favorites.") exit(0) else: logging.error(f"Preset named {_remove_preset} doesn't exist in favorites list. " "Check if you typed the correct preset name.") exit(1) logging.info("Favorite presets list:") for i, preset in enumerate(favorite): print(preset) logging.info(f"Favorites amount: {len(favorite)}") exit(0) def import_preset(self, args): _preset_path = args.preset_path preset_file = GLib.path_get_basename(_preset_path) output_filename = os.path.join(presets_dir, "user", preset_file.strip()) logging.info(f"Importing preset: {preset_file.strip()}") # TODO: Check if preset is already imported if _preset_path.endswith(".json"): try: shutil.copy(_preset_path, output_filename) except FileNotFoundError as e: logging.error("Preset could not be imported.", exc=e) exit(1) else: logging.info("Preset imported successfully.") exit(0) else: logging.error("Unsupported preset file format, must be .json") exit(1) def apply_preset(self, args): #_interactive = args.interactive _preset_name = args.preset_name _preset_path = args.preset_path _gtk = args.gtk try: presets_list = PresetUtils().get_presets_list(full_list=True) except (OSError, KeyError, AttributeError) as e: logging.error("Failed to retrieve a list of presets.", exc=e) exit(1) presets_name = list(presets_list.values()) def __get_preset_from_name(): for path, name in presets_list.items(): if name == _preset_name: preset = Preset().new_from_path(path) return preset if _preset_name: if _preset_name in presets_name: preset = __get_preset_from_name() elif _preset_path: preset = Preset().new_from_path(_preset_path) if _gtk in ("gtk4", "gtk3"): PresetUtils().apply_preset(_gtk, preset) logging.info(f"Preset {preset.display_name} applied successfully for {_gtk.capitalize()} applications.") elif _gtk == "both": PresetUtils().apply_preset("gtk4", preset) PresetUtils().apply_preset("gtk3", preset) logging.info(f"Preset {preset.display_name} applied successfully for Gtk 3 and Gtk 4 applications.") logging.info("In order for changes to take full effect, you need to log out.") exit(0) def new_preset(self, args): #_interactive = args.interactive _name = args.name _colors = args.colors _palette = args.palette _custom_css = args.custom_css _json = args.json # TODO: Do the logic code for `new` command logging.error("This command isn't implemented yet") exit(1) def download_preset(self, args): #_interactive = args.interactive _preset_name = args.preset_name #_custom_url = args.custom_url repo_no = 1 repos_amount = len(preset_repos.items()) for repo_name, repo in preset_repos.items(): try: explore_presets, urls = PresetDownloader().fetch_presets(repo) except (GLib.GError, json.JSONDecodeError) as e: logging.error("An error occurred while fetching presets from remote repository.", exc=e) exit(1) else: preset_no = 1 presets_amount = len(explore_presets.items()) for (preset, preset_name), preset_url in zip(explore_presets.items(), urls): # TODO: Add handling of two or more presets with the same elements in name if _preset_name.lower() in preset_name.lower(): logging.info(f"Downloading preset: {preset_name}") try: PresetDownloader().download_preset(preset_name, to_slug_case(repo_name), preset_url) except (GLib.GError, json.JSONDecodeError, OSError) as e: logging.error("An error occurred while downloading a preset.", exc=e) exit(1) else: logging.info("Preset downloaded successfully.") exit(0) else: if repo_no == repos_amount and preset_no == presets_amount: logging.error(f"No presets found with text: {_preset_name}") exit(1) preset_no += 1 continue repo_no += 1 # NOTE: Possible useful portals to use in future: org.freedesktop.portal.Documents \ # (support missing in libportal, only D-Bus calls), org.freedesktop.portal.FileChooser def generate_monet(self, args): #_apply = args.apply _preset_name = args.preset_name _image_path = args.image_path _tone = args.tone _theme = args.theme _json = args.json try: palette = Monet().generate_from_image(_image_path) except (OSError, ValueError) as e: logging.info("If you are getting an `no such file or directory` error on Gradience installed as Flatpak, " "try adding the file to the access list by using `gradience-cli access-file --allow 'path/to/file'` command.") exit(1) props = [_tone, _theme] if _json: try: preset = PresetUtils().new_preset_from_monet(_preset_name, palette, props, True) except (OSError, AttributeError) as e: logging.error("Unexpected error while generating preset from Monet palette.", exc=e) exit(1) else: preset_json = preset.get_preset_json() print(preset_json) exit(0) try: PresetUtils().new_preset_from_monet(_preset_name, palette, props) #raise OSError() except (OSError, AttributeError) as e: logging.error("Unexpected error while generating preset from Monet palette.", exc=e) exit(1) else: logging.info("Preset generated successfully. " "In order to apply it, use `gradience-cli apply ` command.") exit(0) if _apply: pass # TODO: Add path and xdg-* value parsing def access_file(self, args): _list = args.list _allow = args.allow _disallow = args.disallow if not _list and not _allow and not _disallow: logging.error("You need to specify an argument for this command. " "Type `gradience-cli access-file --help` to check available arguments.") exit(1) if _list: try: access_list = list_file_access() except GLib.GError as e: logging.error("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("No paths found.") exit(0) if _allow: try: allow_file_access(_allow) except GLib.GError as e: logging.error("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("An error occurred while setting file access.", exc=e) exit(1) else: logging.info(f"Path {_disallow} removed from access list.") exit(0) def flatpak_theming(self, args): _enable_theming = args.enable_theming _disable_theming = args.disable_theming if _enable_theming in ("gtk4", "gtk3"): create_gtk_user_override(self.settings, _enable_theming) logging.info(f"Flatpak theming for {_enable_theming.capitalize()} 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 in ("gtk4", "gtk3"): remove_gtk_user_override(self.settings, _disable_theming) logging.info(f"Flatpak theming for {_disable_theming.capitalize()} applications has been disabled.") elif _disable_theming == "both": remove_gtk_user_override(self.settings, "gtk4") remove_gtk_user_override(self.settings, "gtk3") logging.info("Flatpak theming for Gtk 4 and Gtk 3 applications has been disabled.") if __name__ == "__main__": cli = CLI()