aboutsummaryrefslogtreecommitdiff
path: root/pywal
diff options
context:
space:
mode:
authorFuwn <[email protected]>2024-09-12 11:29:21 +0000
committerFuwn <[email protected]>2024-09-12 11:36:10 +0000
commitd487f5e5cb22e58e5ec164e20168b39a75ca39e8 (patch)
treeadf8120847661c79a121848351c304507aeb7899 /pywal
parentfeat: initial release (diff)
downloadpywal.nix-d487f5e5cb22e58e5ec164e20168b39a75ca39e8.tar.xz
pywal.nix-d487f5e5cb22e58e5ec164e20168b39a75ca39e8.zip
feat: more backends
Diffstat (limited to 'pywal')
-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.py259
10 files changed, 932 insertions, 0 deletions
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/pywal/util.py b/pywal/util.py
new file mode 100644
index 0000000..5be5f5c
--- /dev/null
+++ b/pywal/util.py
@@ -0,0 +1,259 @@
+# 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.
+
+"""
+Misc helper functions.
+"""
+import colorsys
+import json
+import logging
+import os
+import platform
+import re
+import shutil
+import subprocess
+import sys
+
+
+class Color:
+ """Color formats."""
+
+ alpha_num = "100"
+
+ def __init__(self, hex_color):
+ self.hex_color = hex_color
+
+ def __str__(self):
+ return self.hex_color
+
+ @property
+ def rgb(self):
+ """Convert a hex color to rgb."""
+ return "%s,%s,%s" % (*hex_to_rgb(self.hex_color),)
+
+ @property
+ def xrgba(self):
+ """Convert a hex color to xrdb rgba."""
+ return hex_to_xrgba(self.hex_color)
+
+ @property
+ def rgba(self):
+ """Convert a hex color to rgba."""
+ return "rgba(%s,%s,%s,%s)" % (*hex_to_rgb(self.hex_color), self.alpha_dec)
+
+ @property
+ def alpha(self):
+ """Add URxvt alpha value to color."""
+ return "[%s]%s" % (self.alpha_num, self.hex_color)
+
+ @property
+ def alpha_dec(self):
+ """Export the alpha value as a decimal number in [0, 1]."""
+ return int(self.alpha_num) / 100
+
+ @property
+ def decimal(self):
+ """Export color in decimal."""
+ return "%s%s" % ("#", int(self.hex_color[1:], 16))
+
+ @property
+ def decimal_strip(self):
+ """Strip '#' from decimal color."""
+ return int(self.hex_color[1:], 16)
+
+ @property
+ def octal(self):
+ """Export color in octal."""
+ return "%s%s" % ("#", oct(int(self.hex_color[1:], 16))[2:])
+
+ @property
+ def octal_strip(self):
+ """Strip '#' from octal color."""
+ return oct(int(self.hex_color[1:], 16))[2:]
+
+ @property
+ def strip(self):
+ """Strip '#' from color."""
+ return self.hex_color[1:]
+
+ @property
+ def red(self):
+ """Red value as float between 0 and 1."""
+ return "%.3f" % (hex_to_rgb(self.hex_color)[0] / 255.0)
+
+ @property
+ def green(self):
+ """Green value as float between 0 and 1."""
+ return "%.3f" % (hex_to_rgb(self.hex_color)[1] / 255.0)
+
+ @property
+ def blue(self):
+ """Blue value as float between 0 and 1."""
+ return "%.3f" % (hex_to_rgb(self.hex_color)[2] / 255.0)
+
+ def lighten(self, percent):
+ """Lighten color by percent."""
+ percent = float(re.sub(r"[\D\.]", "", str(percent)))
+ return Color(lighten_color(self.hex_color, percent / 100))
+
+ def darken(self, percent):
+ """Darken color by percent."""
+ percent = float(re.sub(r"[\D\.]", "", str(percent)))
+ return Color(darken_color(self.hex_color, percent / 100))
+
+ def saturate(self, percent):
+ """Saturate a color."""
+ percent = float(re.sub(r"[\D\.]", "", str(percent)))
+ return Color(saturate_color(self.hex_color, percent / 100))
+
+
+def read_file(input_file):
+ """Read data from a file and trim newlines."""
+ with open(input_file, "r") as file:
+ return file.read().splitlines()
+
+
+def read_file_json(input_file):
+ """Read data from a json file."""
+ with open(input_file, "r") as json_file:
+ return json.load(json_file)
+
+
+def read_file_raw(input_file):
+ """Read data from a file as is, don't strip
+ newlines or other special characters."""
+ with open(input_file, "r") as file:
+ return file.readlines()
+
+
+def save_file(data, export_file):
+ """Write data to a file."""
+ create_dir(os.path.dirname(export_file))
+
+ try:
+ with open(export_file, "w") as file:
+ file.write(data)
+ except PermissionError:
+ logging.warning("Couldn't write to %s.", export_file)
+
+
+def save_file_json(data, export_file):
+ """Write data to a json file."""
+ create_dir(os.path.dirname(export_file))
+
+ with open(export_file, "w") as file:
+ json.dump(data, file, indent=4)
+
+
+def create_dir(directory):
+ """Alias to create the cache dir."""
+ os.makedirs(directory, exist_ok=True)
+
+
+def setup_logging():
+ """Logging config."""
+ logging.basicConfig(
+ format=(
+ "[%(levelname)s\033[0m] " "\033[1;31m%(module)s\033[0m: " "%(message)s"
+ ),
+ level=logging.INFO,
+ stream=sys.stdout,
+ )
+ logging.addLevelName(logging.ERROR, "\033[1;31mE")
+ logging.addLevelName(logging.INFO, "\033[1;32mI")
+ logging.addLevelName(logging.WARNING, "\033[1;33mW")
+
+
+def hex_to_rgb(color):
+ """Convert a hex color to rgb."""
+ return tuple(bytes.fromhex(color.strip("#")))
+
+
+def hex_to_xrgba(color):
+ """Convert a hex color to xrdb rgba."""
+ col = color.lower().strip("#")
+ return "%s%s/%s%s/%s%s/ff" % (*col,)
+
+
+def rgb_to_hex(color):
+ """Convert an rgb color to hex."""
+ return "#%02x%02x%02x" % (*color,)
+
+
+def darken_color(color, amount):
+ """Darken a hex color."""
+ color = [int(col * (1 - amount)) for col in hex_to_rgb(color)]
+ return rgb_to_hex(color)
+
+
+def lighten_color(color, amount):
+ """Lighten a hex color."""
+ color = [int(col + (255 - col) * amount) for col in hex_to_rgb(color)]
+ return rgb_to_hex(color)
+
+
+def blend_color(color, color2):
+ """Blend two colors together."""
+ r1, g1, b1 = hex_to_rgb(color)
+ r2, g2, b2 = hex_to_rgb(color2)
+
+ r3 = int(0.5 * r1 + 0.5 * r2)
+ g3 = int(0.5 * g1 + 0.5 * g2)
+ b3 = int(0.5 * b1 + 0.5 * b2)
+
+ return rgb_to_hex((r3, g3, b3))
+
+
+def saturate_color(color, amount):
+ """Saturate a hex color."""
+ r, g, b = hex_to_rgb(color)
+ r, g, b = [x / 255.0 for x in (r, g, b)]
+ h, l, s = colorsys.rgb_to_hls(r, g, b)
+ s = amount
+ r, g, b = colorsys.hls_to_rgb(h, l, s)
+ r, g, b = [x * 255.0 for x in (r, g, b)]
+
+ return rgb_to_hex((int(r), int(g), int(b)))
+
+
+def rgb_to_yiq(color):
+ """Sort a list of colors."""
+ return colorsys.rgb_to_yiq(*hex_to_rgb(color))
+
+
+def disown(cmd):
+ """Call a system command in the background,
+ disown it and hide it's output."""
+ subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
+
+
+def get_pid(name):
+ """Check if process is running by name."""
+ if not shutil.which("pidof"):
+ return False
+
+ try:
+ if platform.system() != "Darwin":
+ subprocess.check_output(["pidof", "-s", name])
+ else:
+ subprocess.check_output(["pidof", name])
+
+ except subprocess.CalledProcessError:
+ return False
+
+ return True