aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFuwn <[email protected]>2024-09-12 04:29:21 -0700
committerFuwn <[email protected]>2024-09-12 04:36:10 -0700
commit70cffdc698485f3bce893c1cefe75d4e793b025f (patch)
treeadf8120847661c79a121848351c304507aeb7899
parent836627860d532e986a82e4fc62744850a19cde62 (diff)
downloadpywal.nix-70cffdc698485f3bce893c1cefe75d4e793b025f.tar.xz
pywal.nix-70cffdc698485f3bce893c1cefe75d4e793b025f.zip
feat: more backends
-rw-r--r--.gitignore1
-rw-r--r--README.md21
-rw-r--r--flake.nix19
-rw-r--r--pywal/LICENSE.md9
-rw-r--r--pywal/__init__.py0
-rw-r--r--pywal/backends/__init__.py0
-rw-r--r--pywal/backends/colorthief.py79
-rw-r--r--pywal/backends/colorz.py60
-rw-r--r--pywal/backends/wal.py105
-rw-r--r--pywal/colors.py185
-rw-r--r--pywal/settings.py46
-rw-r--r--pywal/theme.py189
-rw-r--r--pywal/util.py (renamed from wal.py)96
-rw-r--r--shell.nix10
-rw-r--r--wrap.py18
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__
diff --git a/README.md b/README.md
index d06606a..c191d76 100644
--- a/README.md
+++ b/README.md
@@ -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
diff --git a/flake.nix b/flake.nix
index 7503151..c5921ca 100644
--- a/flake.nix
+++ b/flake.nix
@@ -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)
diff --git a/wal.py b/pywal/util.py
index 744c555..5be5f5c 100644
--- a/wal.py
+++ b/pywal/util.py
@@ -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
+ ];
+}
diff --git a/wrap.py b/wrap.py
new file mode 100644
index 0000000..dbe0fcb
--- /dev/null
+++ b/wrap.py
@@ -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,
+