Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added wallpaper checksum generation and comparison for cached color schemes #33

Merged
merged 2 commits into from
Apr 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 27 additions & 14 deletions pywal/colors.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""
Generate a palette using various backends.
"""

import logging
import os
import random
Expand All @@ -14,31 +15,34 @@

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]
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':
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.replace("\\", "/")
return img


def colors_to_dict(colors, img):
"""Convert list of colors to pywal format."""
return {
"checksum": util.get_img_checksum(img),
"wallpaper": normalize_img_path(img),
"alpha": util.Color.alpha_num,

"special": {
"background": colors[0],
"foreground": colors[15],
"cursor": colors[15]
"cursor": colors[15],
},

"colors": {
"color0": colors[0],
"color1": colors[1],
Expand All @@ -55,8 +59,8 @@ def colors_to_dict(colors, img):
"color12": colors[12],
"color13": colors[13],
"color14": colors[14],
"color15": colors[15]
}
"color15": colors[15],
},
}


Expand Down Expand Up @@ -126,7 +130,6 @@ def generic_adjust(colors, light, cols16):
colors[8] = util.lighten_color(colors[0], 0.25)
colors[15] = colors[7]


return colors


Expand All @@ -147,8 +150,15 @@ def cache_fname(img, backend, cols16, light, cache_dir, sat=""):
file_name = re.sub("[/|\\|.]", "_", img)
file_size = os.path.getsize(img)

file_parts = [file_name, color_num, color_type, backend,
sat, file_size, __cache_version__]
file_parts = [
file_name,
color_num,
color_type,
backend,
sat,
file_size,
__cache_version__,
]
return [cache_dir, "schemes", "%s_%s_%s_%s_%s_%s_%s.json" % (*file_parts,)]


Expand Down Expand Up @@ -182,7 +192,10 @@ def get(img, light=False, cols16=False, backend="wal", cache_dir=CACHE_DIR, sat=
cache_name = cache_fname(img, backend, cols16, light, cache_dir, sat)
cache_file = os.path.join(*cache_name)

if os.path.isfile(cache_file):
# Check the wallpaper's checksum against the cache'
if os.path.isfile(cache_file) and theme.parse(cache_file)[
"checksum"
] == util.get_img_checksum(img):
colors = theme.file(cache_file)
colors["alpha"] = util.Color.alpha_num
logging.info("Found cached colorscheme.")
Expand Down
66 changes: 43 additions & 23 deletions pywal/theme.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""
Theme file handling.
"""

import logging
import os
import random
Expand All @@ -12,31 +13,44 @@

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()]
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", "")
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(
" -",
"\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(
" -",
"\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(
" -",
"\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)")
Expand All @@ -54,8 +68,10 @@ def list_themes(dark=True):

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/"))]
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)]


Expand All @@ -65,7 +81,7 @@ def terminal_sexy_to_wal(data):
data["special"] = {
"foreground": data["foreground"],
"background": data["background"],
"cursor": data["color"][9]
"cursor": data["color"][9],
}

for i, color in enumerate(data["color"]):
Expand All @@ -78,6 +94,9 @@ def parse(theme_file):
"""Parse the theme file."""
data = util.read_file_json(theme_file)

if "checksum" not in data:
data["checksum"] = "None"

if "wallpaper" not in data:
data["wallpaper"] = "None"

Expand Down Expand Up @@ -134,10 +153,10 @@ def file(input_file, light=False):

# 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"))
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)
Expand All @@ -149,6 +168,7 @@ def file(input_file, light=False):
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)
theme_path = os.path.join(
CONF_DIR, "colorschemes", "light" if light else "dark", theme_file
)
util.save_file_json(colors, theme_path)
57 changes: 33 additions & 24 deletions pywal/util.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""
Misc helper functions.
"""

import colorsys
import json
import logging
Expand All @@ -10,10 +11,12 @@
import shutil
import subprocess
import sys
from hashlib import md5


class Color:
"""Color formats."""

alpha_num = "100"

def __init__(self, hex_color):
Expand All @@ -40,14 +43,12 @@ def xrgba(self):
@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)
return "rgba(%s,%s,%s,%s)" % (*hex_to_rgb(self.hex_color), self.alpha_dec)

@property
def hex_argb(self):
"""Convert an alpha hex color to argb hex."""
return "#%02X%s" % (int(int(self.alpha_num) * 255 / 100),
self.hex_color[1:])
return "#%02X%s" % (int(int(self.alpha_num) * 255 / 100), self.hex_color[1:])

@property
def alpha(self):
Expand Down Expand Up @@ -92,17 +93,17 @@ def strip(self):
@property
def red(self):
"""Red value as float between 0 and 1."""
return "%.3f" % (hex_to_rgb(self.hex_color)[0]/255.)
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.)
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.)
return "%.3f" % (hex_to_rgb(self.hex_color)[2] / 255.0)

@property
def red_hex(self):
Expand Down Expand Up @@ -136,17 +137,17 @@ def blue_dec(self):

def lighten(self, percent):
"""Lighten color by percent."""
percent = float(re.sub(r'[\D\.]', '', str(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)))
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)))
percent = float(re.sub(r"[\D\.]", "", str(percent)))
return Color(saturate_color(self.hex_color, percent / 100))


Expand All @@ -164,7 +165,7 @@ def read_file_json(input_file):

def read_file_raw(input_file):
"""Read data from a file as is, don't strip
newlines or other special characters."""
newlines or other special characters."""
with open(input_file, "r") as file:
return file.readlines()

Expand All @@ -188,21 +189,31 @@ def save_file_json(data, export_file):
json.dump(data, file, indent=4)


def get_img_checksum(img):
checksum = md5(usedforsecurity=False)
with open(img, "rb") as f:
for chunk in iter(lambda: f.read(4096), b""):
checksum.update(chunk)
return checksum.hexdigest()


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')
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):
Expand Down Expand Up @@ -264,10 +275,8 @@ def rgb_to_yiq(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)
disown it and hide it's output."""
subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)


def get_pid(name):
Expand All @@ -276,7 +285,7 @@ def get_pid(name):
return False

try:
if platform.system() != 'Darwin':
if platform.system() != "Darwin":
subprocess.check_output(["pidof", "-s", name])
else:
subprocess.check_output(["pidof", name])
Expand Down
11 changes: 11 additions & 0 deletions tests/test_colors.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Test imagemagick functions."""

import unittest

from pywal import colors
Expand Down Expand Up @@ -27,6 +28,16 @@ def test_color_import_no_alpha(self):
result = colors.file("tests/test_files/test_file2.json")
self.assertEqual(result["alpha"], "100")

def test_color_import_no_checkum(self):
"""> Read checksum from a file with no checksum"""
result = colors.file("tests/test_files/test_file.json")
self.assertEqual(result["checksum"], "None")

def test_gen_colors_checksum(self):
"""> Generate a colorscheme with the wallpaper's checksum"""
result = colors.get("tests/test_files/test.jpg")
self.assertEqual(len(result["checksum"]), 32)


if __name__ == "__main__":
unittest.main()
Loading