diff options
| author | Fuwn <[email protected]> | 2024-09-12 04:29:21 -0700 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2024-09-12 04:36:10 -0700 |
| commit | 70cffdc698485f3bce893c1cefe75d4e793b025f (patch) | |
| tree | adf8120847661c79a121848351c304507aeb7899 | |
| parent | 836627860d532e986a82e4fc62744850a19cde62 (diff) | |
| download | pywal.nix-70cffdc698485f3bce893c1cefe75d4e793b025f.tar.xz pywal.nix-70cffdc698485f3bce893c1cefe75d4e793b025f.zip | |
feat: more backends
| -rw-r--r-- | .gitignore | 1 | ||||
| -rw-r--r-- | README.md | 21 | ||||
| -rw-r--r-- | flake.nix | 19 | ||||
| -rw-r--r-- | pywal/LICENSE.md | 9 | ||||
| -rw-r--r-- | pywal/__init__.py | 0 | ||||
| -rw-r--r-- | pywal/backends/__init__.py | 0 | ||||
| -rw-r--r-- | pywal/backends/colorthief.py | 79 | ||||
| -rw-r--r-- | pywal/backends/colorz.py | 60 | ||||
| -rw-r--r-- | pywal/backends/wal.py | 105 | ||||
| -rw-r--r-- | pywal/colors.py | 185 | ||||
| -rw-r--r-- | pywal/settings.py | 46 | ||||
| -rw-r--r-- | pywal/theme.py | 189 | ||||
| -rw-r--r-- | pywal/util.py (renamed from wal.py) | 96 | ||||
| -rw-r--r-- | shell.nix | 10 | ||||
| -rw-r--r-- | wrap.py | 18 |
15 files changed, 731 insertions, 107 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bee8a64 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +__pycache__ @@ -6,8 +6,7 @@ Pywal colourschemes into any Home Manager configuration at evaluation-time. I needed a pure (not `--impure`) Nix solution. I made a pure Nix solution. It -works well. I'll likely hack on it some more, even though I personally only use -the `wal` backend. +works well. ## Usage @@ -53,6 +52,7 @@ the `pywal-nix` attribute. pywal-nix = { wallpaper = /path/to/wallpaper.png; # Required light = false; # Defaults to false + backend = "wal"; # One of "wal", "colorz", or "colorthief"; Defaults to "wal" }; # Example usage to print out two colourscheme colours @@ -66,13 +66,16 @@ the `pywal-nix` attribute. ## Pywal -This project combines the -[`pywal/backends/wal.py`](https://github.com/dylanaraps/pywal/blob/master/pywal/backends/wal.py) -and -[`pywal/util.py`](https://github.com/dylanaraps/pywal/blob/master/pywal/util.py) -files from [dylanaraps/pywal](https://github.com/dylanaraps/pywal) into the -[`wal.py`](./wal.py) file. Pywal is licensed under the -[MIT License](https://github.com/dylanaraps/pywal/blob/master/LICENSE.md). +This project includes multiple files from +[dylanaraps/pywal](https://github.com/dylanaraps/pywal), a project which is +licensed under the [MIT License](./pywal/LICENSE.md). + +- [`pywal/backends/colorthief.py`](https://github.com/dylanaraps/pywal/blob/master/pywal/backends/colorthief.py) +- [`pywal/backends/colorz.py`](https://github.com/dylanaraps/pywal/blob/master/pywal/backends/colorz.py) +- [`pywal/backends/wal.py`](https://github.com/dylanaraps/pywal/blob/master/pywal/backends/wal.py) +- [`pywal/colors.py`](https://github.com/dylanaraps/pywal/blob/master/pywal/colors.py) +- [`pywal/theme.py`](https://github.com/dylanaraps/pywal/blob/master/pywal/theme.py) +- [`pywal/util.py`](https://github.com/dylanaraps/pywal/blob/master/pywal/util.py) ## Licence @@ -25,12 +25,17 @@ buildInputs = with pkgs; [ imagemagick jq + python312Packages.colorthief + colorz ]; } '' - mkdir -p $out + mkdir -p $out/wrapper - ${pkgs.python3}/bin/python3 ${./wal.py} ${config.pywal-nix.wallpaper} ${ + cp ${./wrap.py} $out/wrapper/wrap.py + cp -r ${./pywal} $out/wrapper/pywal + + ${pkgs.python3}/bin/python3 $out/wrapper/wrap.py ${config.pywal-nix.backend} ${config.pywal-nix.wallpaper} ${ if config.pywal-nix.light then "1" else "0" } | \ sed "s/'/\"/g" | \ @@ -46,6 +51,16 @@ default = /path/to/wallpaper.png; }; + backend = lib.mkOption { + type = lib.types.enum [ + "colorthief" + "colorz" + "wal" + ]; + + default = "wal"; + }; + light = lib.mkOption { type = lib.types.bool; default = false; diff --git a/pywal/LICENSE.md b/pywal/LICENSE.md new file mode 100644 index 0000000..03c945e --- /dev/null +++ b/pywal/LICENSE.md @@ -0,0 +1,9 @@ +The MIT License (MIT) + +Copyright (c) 2017 Dylan Araps + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/pywal/__init__.py b/pywal/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/pywal/__init__.py diff --git a/pywal/backends/__init__.py b/pywal/backends/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/pywal/backends/__init__.py diff --git a/pywal/backends/colorthief.py b/pywal/backends/colorthief.py new file mode 100644 index 0000000..f1d52f7 --- /dev/null +++ b/pywal/backends/colorthief.py @@ -0,0 +1,79 @@ +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the "Software"), +# to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, +# and/or sell copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. + +""" +Generate a colorscheme using ColorThief. +""" + +import logging +import sys + +try: + from colorthief import ColorThief + +except ImportError: + logging.error("ColorThief wasn't found on your system.") + logging.error("Try another backend. (wal --backend)") + sys.exit(1) + +from .. import util + + +def gen_colors(img): + """Loop until 16 colors are generated.""" + color_cmd = ColorThief(img).get_palette + + for i in range(0, 10, 1): + raw_colors = color_cmd(color_count=8 + i) + + if len(raw_colors) >= 8: + break + + if i == 10: + logging.error("ColorThief couldn't generate a suitable palette.") + sys.exit(1) + + return [util.rgb_to_hex(color) for color in raw_colors] + + +def adjust(cols, light): + """Create palette.""" + cols.sort(key=util.rgb_to_yiq) + raw_colors = [*cols, *cols] + + if light: + raw_colors[0] = util.lighten_color(cols[0], 0.90) + raw_colors[7] = util.darken_color(cols[0], 0.75) + + else: + for color in raw_colors: + color = util.lighten_color(color, 0.40) + + raw_colors[0] = util.darken_color(cols[0], 0.80) + raw_colors[7] = util.lighten_color(cols[0], 0.60) + + raw_colors[8] = util.lighten_color(cols[0], 0.20) + raw_colors[15] = raw_colors[7] + + return raw_colors + + +def get(img, light=False): + """Get colorscheme.""" + cols = gen_colors(img) + return adjust(cols, light) diff --git a/pywal/backends/colorz.py b/pywal/backends/colorz.py new file mode 100644 index 0000000..f05e534 --- /dev/null +++ b/pywal/backends/colorz.py @@ -0,0 +1,60 @@ +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the "Software"), +# to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, +# and/or sell copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. + +""" +Generate a colorscheme using Colorz. +""" +import logging +import sys + +try: + import colorz + +except ImportError: + logging.error("colorz wasn't found on your system.") + logging.error("Try another backend. (wal --backend)") + sys.exit(1) + +from .. import colors +from .. import util + + +def gen_colors(img): + """Generate a colorscheme using Colorz.""" + # pylint: disable=not-callable + raw_colors = colorz.colorz(img, n=6, bold_add=0) + return [util.rgb_to_hex([*color[0]]) for color in raw_colors] + + +def adjust(cols, light): + """Create palette.""" + raw_colors = [cols[0], *cols, "#FFFFFF", "#000000", *cols, "#FFFFFF"] + + return colors.generic_adjust(raw_colors, light) + + +def get(img, light=False): + """Get colorscheme.""" + cols = gen_colors(img) + + if len(cols) < 6: + logging.error("colorz failed to generate enough colors.") + logging.error("Try another backend or another image. (wal --backend)") + sys.exit(1) + + return adjust(cols, light) diff --git a/pywal/backends/wal.py b/pywal/backends/wal.py new file mode 100644 index 0000000..883abb6 --- /dev/null +++ b/pywal/backends/wal.py @@ -0,0 +1,105 @@ +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the "Software"), +# to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, +# and/or sell copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. + +""" +Generate a colorscheme using imagemagick. +""" + +import logging +import re +import shutil +import subprocess +import sys + +from .. import util + + +def imagemagick(color_count, img, magick_command): + """Call Imagemagick to generate a scheme.""" + flags = ["-resize", "25%", "-colors", str(color_count), "-unique-colors", "txt:-"] + img += "[0]" + + return subprocess.check_output([*magick_command, img, *flags]).splitlines() + + +def has_im(): + """Check to see if the user has im installed.""" + if shutil.which("magick"): + return ["magick"] + + if shutil.which("convert"): + return ["convert"] + + logging.error("Imagemagick wasn't found on your system.") + logging.error("Try another backend. (wal --backend)") + sys.exit(1) + + +def gen_colors(img): + """Format the output from imagemagick into a list + of hex colors.""" + magick_command = has_im() + + for i in range(0, 20, 1): + raw_colors = imagemagick(16 + i, img, magick_command) + + if len(raw_colors) > 16: + break + + if i == 19: + logging.error("Imagemagick couldn't generate a suitable palette.") + sys.exit(1) + + else: + logging.warning("Imagemagick couldn't generate a palette.") + logging.warning("Trying a larger palette size %s", 16 + i) + + return [re.search("#.{6}", str(col)).group(0) for col in raw_colors[1:]] + + +def adjust(colors, light): + """Adjust the generated colors and store them in a dict that + we will later save in json format.""" + raw_colors = colors[:1] + colors[8:16] + colors[8:-1] + + # Manually adjust colors. + if light: + for color in raw_colors: + color = util.saturate_color(color, 0.5) + + raw_colors[0] = util.lighten_color(colors[-1], 0.85) + raw_colors[7] = colors[0] + raw_colors[8] = util.darken_color(colors[-1], 0.4) + raw_colors[15] = colors[0] + + else: + # Darken the background color slightly. + if raw_colors[0][1] != "0": + raw_colors[0] = util.darken_color(raw_colors[0], 0.40) + + raw_colors[7] = util.blend_color(raw_colors[7], "#EEEEEE") + raw_colors[8] = util.darken_color(raw_colors[7], 0.30) + raw_colors[15] = util.blend_color(raw_colors[15], "#EEEEEE") + + return raw_colors + + +def get(img, light=False): + """Get colorscheme.""" + colors = gen_colors(img) + return adjust(colors, light) diff --git a/pywal/colors.py b/pywal/colors.py new file mode 100644 index 0000000..33f6b6b --- /dev/null +++ b/pywal/colors.py @@ -0,0 +1,185 @@ +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the "Software"), +# to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, +# and/or sell copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. + +""" +Generate a palette using various backends. +""" + +import logging +import os +import random +import re +import sys + +from . import theme +from . import util +from .settings import CACHE_DIR, MODULE_DIR, __cache_version__ + + +def list_backends(): + """List color backends.""" + return [ + b.name.replace(".py", "") + for b in os.scandir(os.path.join(MODULE_DIR, "backends")) + if "__" not in b.name + ] + + +def normalize_img_path(img: str): + """Normalizes the image path for output.""" + if os.name == "nt": + # On Windows, the JSON.dump ends up outputting un-escaped backslash breaking + # the ability to read colors.json. Windows supports forward slash, so we can + # use that for now + return img.replace("\\", "/") + return img + + +def colors_to_dict(colors, img): + """Convert list of colors to pywal format.""" + return { + "wallpaper": normalize_img_path(img), + "alpha": util.Color.alpha_num, + "special": { + "background": colors[0], + "foreground": colors[15], + "cursor": colors[15], + }, + "colors": { + "color0": colors[0], + "color1": colors[1], + "color2": colors[2], + "color3": colors[3], + "color4": colors[4], + "color5": colors[5], + "color6": colors[6], + "color7": colors[7], + "color8": colors[8], + "color9": colors[9], + "color10": colors[10], + "color11": colors[11], + "color12": colors[12], + "color13": colors[13], + "color14": colors[14], + "color15": colors[15], + }, + } + + +def generic_adjust(colors, light): + """Generic color adjustment for themers.""" + if light: + for color in colors: + color = util.saturate_color(color, 0.60) + color = util.darken_color(color, 0.5) + + colors[0] = util.lighten_color(colors[0], 0.95) + colors[7] = util.darken_color(colors[0], 0.75) + colors[8] = util.darken_color(colors[0], 0.25) + colors[15] = colors[7] + + else: + colors[0] = util.darken_color(colors[0], 0.80) + colors[7] = util.lighten_color(colors[0], 0.75) + colors[8] = util.lighten_color(colors[0], 0.25) + colors[15] = colors[7] + + return colors + + +def saturate_colors(colors, amount): + """Saturate all colors.""" + if amount and float(amount) <= 1.0: + for i, _ in enumerate(colors): + if i not in [0, 7, 8, 15]: + colors[i] = util.saturate_color(colors[i], float(amount)) + + return colors + + +def cache_fname(img, backend, light, cache_dir, sat=""): + """Create the cache file name.""" + color_type = "light" if light else "dark" + file_name = re.sub("[/|\\|.]", "_", img) + file_size = os.path.getsize(img) + + file_parts = [file_name, color_type, backend, sat, file_size, __cache_version__] + return [cache_dir, "schemes", "%s_%s_%s_%s_%s_%s.json" % (*file_parts,)] + + +def get_backend(backend): + """Figure out which backend to use.""" + if backend == "random": + backends = list_backends() + random.shuffle(backends) + return backends[0] + + return backend + + +def palette(): + """Generate a palette from the colors.""" + for i in range(0, 16): + if i % 8 == 0: + print() + + if i > 7: + i = "8;5;%s" % i + + print("\033[4%sm%s\033[0m" % (i, " " * (80 // 20)), end="") + + print("\n") + + +def get(img, light=False, backend="wal", cache_dir=CACHE_DIR, sat=""): + """Generate a palette.""" + # home_dylan_img_jpg_backend_1.2.2.json + cache_name = cache_fname(img, backend, light, cache_dir, sat) + cache_file = os.path.join(*cache_name) + + if os.path.isfile(cache_file): + colors = theme.file(cache_file) + colors["alpha"] = util.Color.alpha_num + logging.info("Found cached colorscheme.") + + else: + logging.info("Generating a colorscheme.") + backend = get_backend(backend) + + # Dynamically import the backend we want to use. + # This keeps the dependencies "optional". + try: + __import__("pywal.backends.%s" % backend) + except ImportError: + __import__("pywal.backends.wal") + backend = "wal" + + logging.info("Using %s backend.", backend) + backend = sys.modules["pywal.backends.%s" % backend] + colors = getattr(backend, "get")(img, light) + colors = colors_to_dict(saturate_colors(colors, sat), img) + + util.save_file_json(colors, cache_file) + logging.info("Generation complete.") + + return colors + + +def file(input_file): + """Deprecated: symbolic link to --> theme.file""" + return theme.file(input_file) diff --git a/pywal/settings.py b/pywal/settings.py new file mode 100644 index 0000000..68f8dae --- /dev/null +++ b/pywal/settings.py @@ -0,0 +1,46 @@ +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the "Software"), +# to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, +# and/or sell copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. + +""" + '|| +... ... .... ... ... ... ... .... || + ||' || '|. | || || | '' .|| || + || | '|.| ||| ||| .|' || || + ||...' '| | | '|..'|' .||. + || .. | +'''' '' +Created by Dylan Araps. +""" + +import os +import platform + + +__version__ = "3.3.1" +__cache_version__ = "1.1.0" + + +HOME = os.getenv("HOME", os.getenv("USERPROFILE")) +XDG_CACHE_DIR = os.getenv("XDG_CACHE_HOME", os.path.join(HOME, ".cache")) +XDG_CONF_DIR = os.getenv("XDG_CONFIG_HOME", os.path.join(HOME, ".config")) + +CACHE_DIR = os.getenv("PYWAL_CACHE_DIR", os.path.join(XDG_CACHE_DIR, "wal")) +CONF_DIR = os.path.join(XDG_CONF_DIR, "wal") +MODULE_DIR = os.path.dirname(__file__) + +OS = platform.uname()[0] diff --git a/pywal/theme.py b/pywal/theme.py new file mode 100644 index 0000000..94646cf --- /dev/null +++ b/pywal/theme.py @@ -0,0 +1,189 @@ +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the "Software"), +# to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, +# and/or sell copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. + +""" +Theme file handling. +""" + +import logging +import os +import random +import sys + +from .settings import CACHE_DIR, CONF_DIR, MODULE_DIR +from . import util + + +def list_out(): + """List all themes in a pretty format.""" + dark_themes = [theme.name.replace(".json", "") for theme in list_themes()] + ligh_themes = [theme.name.replace(".json", "") for theme in list_themes(dark=False)] + user_themes = [theme.name.replace(".json", "") for theme in list_themes_user()] + + try: + last_used_theme = util.read_file(os.path.join(CACHE_DIR, "last_used_theme"))[ + 0 + ].replace(".json", "") + except FileNotFoundError: + last_used_theme = "" + + if user_themes: + print("\033[1;32mUser Themes\033[0m:") + print( + " -", + "\n - ".join( + t + " (last used)" if t == last_used_theme else t + for t in sorted(user_themes) + ), + ) + + print("\033[1;32mDark Themes\033[0m:") + print( + " -", + "\n - ".join( + t + " (last used)" if t == last_used_theme else t + for t in sorted(dark_themes) + ), + ) + + print("\033[1;32mLight Themes\033[0m:") + print( + " -", + "\n - ".join( + t + " (last used)" if t == last_used_theme else t + for t in sorted(ligh_themes) + ), + ) + + print("\033[1;32mExtra\033[0m:") + print(" - random (select a random dark theme)") + print(" - random_dark (select a random dark theme)") + print(" - random_light (select a random light theme)") + print(" - random_user (select a random user theme)") + + +def list_themes(dark=True): + """List all installed theme files.""" + dark = "dark" if dark else "light" + themes = os.scandir(os.path.join(MODULE_DIR, "colorschemes", dark)) + return [t for t in themes if os.path.isfile(t.path)] + + +def list_themes_user(): + """List user theme files.""" + themes = [ + *os.scandir(os.path.join(CONF_DIR, "colorschemes/dark/")), + *os.scandir(os.path.join(CONF_DIR, "colorschemes/light/")), + ] + return [t for t in themes if os.path.isfile(t.path)] + + +def terminal_sexy_to_wal(data): + """Convert terminal.sexy json schema to wal.""" + data["colors"] = {} + data["special"] = { + "foreground": data["foreground"], + "background": data["background"], + "cursor": data["color"][9], + } + + for i, color in enumerate(data["color"]): + data["colors"]["color%s" % i] = color + + return data + + +def parse(theme_file): + """Parse the theme file.""" + data = util.read_file_json(theme_file) + + if "wallpaper" not in data: + data["wallpaper"] = "None" + + if "alpha" not in data: + data["alpha"] = util.Color.alpha_num + + # Terminal.sexy format. + if "color" in data: + data = terminal_sexy_to_wal(data) + + return data + + +def get_random_theme(dark=True): + """Get a random theme file.""" + themes = [theme.path for theme in list_themes(dark)] + random.shuffle(themes) + return themes[0] + + +def get_random_theme_user(): + """Get a random theme file from user theme directories.""" + themes = [theme.path for theme in list_themes_user()] + random.shuffle(themes) + return themes[0] + + +def file(input_file, light=False): + """Import colorscheme from json file.""" + util.create_dir(os.path.join(CONF_DIR, "colorschemes/light/")) + util.create_dir(os.path.join(CONF_DIR, "colorschemes/dark/")) + + theme_name = ".".join((input_file, "json")) + bri = "light" if light else "dark" + + user_theme_file = os.path.join(CONF_DIR, "colorschemes", bri, theme_name) + theme_file = os.path.join(MODULE_DIR, "colorschemes", bri, theme_name) + + # Find the theme file. + if input_file in ("random", "random_dark"): + theme_file = get_random_theme() + + elif input_file == "random_light": + theme_file = get_random_theme(light) + + elif input_file == "random_user": + theme_file = get_random_theme_user() + + elif os.path.isfile(user_theme_file): + theme_file = user_theme_file + + elif os.path.isfile(input_file): + theme_file = input_file + + # Parse the theme file. + if os.path.isfile(theme_file): + logging.info("Set theme to \033[1;37m%s\033[0m.", os.path.basename(theme_file)) + util.save_file( + os.path.basename(theme_file), os.path.join(CACHE_DIR, "last_used_theme") + ) + return parse(theme_file) + + logging.error("No %s colorscheme file found.", bri) + logging.error("Try adding '-l' to set light themes.") + logging.error("Try removing '-l' to set dark themes.") + sys.exit(1) + + +def save(colors, theme_name, light=False): + """Save colors to a theme file.""" + theme_file = theme_name + ".json" + theme_path = os.path.join( + CONF_DIR, "colorschemes", "light" if light else "dark", theme_file + ) + util.save_file_json(colors, theme_path) @@ -15,11 +15,6 @@ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER # DEALINGS IN THE SOFTWARE. -# -# https://github.com/dylanaraps/pywal -# https://github.com/dylanaraps/pywal/blob/master/pywal/backends/wal.py -# https://github.com/dylanaraps/pywal/blob/master/pywal/util.py - """ Misc helper functions. @@ -262,94 +257,3 @@ def get_pid(name): return False return True - - -""" -Generate a colorscheme using imagemagick. -""" - -import logging -import re -import shutil -import subprocess -import sys - - -def imagemagick(color_count, img, magick_command): - """Call Imagemagick to generate a scheme.""" - flags = ["-resize", "25%", "-colors", str(color_count), "-unique-colors", "txt:-"] - img += "[0]" - - return subprocess.check_output([*magick_command, img, *flags]).splitlines() - - -def has_im(): - """Check to see if the user has im installed.""" - if shutil.which("magick"): - return ["magick"] - - if shutil.which("convert"): - return ["convert"] - - logging.error("Imagemagick wasn't found on your system.") - logging.error("Try another backend. (wal --backend)") - sys.exit(1) - - -def gen_colors(img): - """Format the output from imagemagick into a list - of hex colors.""" - magick_command = has_im() - - for i in range(0, 20, 1): - raw_colors = imagemagick(16 + i, img, magick_command) - - if len(raw_colors) > 16: - break - - if i == 19: - logging.error("Imagemagick couldn't generate a suitable palette.") - sys.exit(1) - - else: - logging.warning("Imagemagick couldn't generate a palette.") - logging.warning("Trying a larger palette size %s", 16 + i) - - return [re.search("#.{6}", str(col)).group(0) for col in raw_colors[1:]] - - -def adjust(colors, light): - """Adjust the generated colors and store them in a dict that - we will later save in json format.""" - raw_colors = colors[:1] + colors[8:16] + colors[8:-1] - - # Manually adjust colors. - if light: - for color in raw_colors: - color = saturate_color(color, 0.5) - - raw_colors[0] = lighten_color(colors[-1], 0.85) - raw_colors[7] = colors[0] - raw_colors[8] = darken_color(colors[-1], 0.4) - raw_colors[15] = colors[0] - - else: - # Darken the background color slightly. - if raw_colors[0][1] != "0": - raw_colors[0] = darken_color(raw_colors[0], 0.40) - - raw_colors[7] = blend_color(raw_colors[7], "#EEEEEE") - raw_colors[8] = darken_color(raw_colors[7], 0.30) - raw_colors[15] = blend_color(raw_colors[15], "#EEEEEE") - - return raw_colors - - -def get(img, light=False): - """Get colorscheme.""" - colors = gen_colors(img) - return adjust(colors, light) - - -if __name__ == "__main__" and len(sys.argv) > 1: - print(get(sys.argv[1], sys.argv[2] == "1" if len(sys.argv) > 2 else False)) diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..f86a120 --- /dev/null +++ b/shell.nix @@ -0,0 +1,10 @@ +{ + pkgs ? import <nixpkgs> { }, +}: +pkgs.mkShell { + packages = with pkgs; [ + imagemagick + python312Packages.colorthief + colorz + ]; +} @@ -0,0 +1,18 @@ +import sys + +from pywal.backends import ( + colorthief, + colorz, + wal, +) + +if __name__ == "__main__" and len(sys.argv) > 1: + print( + { + "colorthief": colorthief.get, + |