dotfiles/bin/export-wal-theme
2020-05-21 23:16:15 +08:00

244 lines
7.7 KiB
Python
Executable File

#!/usr/bin/env python
# This simple Python 3 script simply generates a color scheme in wpgtk style with the auto-adjusted colors.
# I like how wpgtk generates the color palette but I don't like the workflow of it.
# I like the simple workflow of pywal but I don't like the colors generated by it and I think it has built-in limitations for generating colors.
# So I combined both of the best in this one glued script.
# The command line program simply needs an image path as the argument — `export-theme ~/Pictures/mountain.jpg`.
# It will also export the theme as a JSON file in the current directory — `mountain.json`.
# Most of the code are "borrowed" from the wpgtk codebase and I've simply studied them and added some documentation.
# It simply needs pywal as a dependency.
import argparse
from colorsys import rgb_to_hls, hls_to_rgb
import json
import math
from operator import itemgetter
import os.path
from pathlib import Path
import sys
import pywal
###################
# COLOR FUNCTIONS #
###################
def hls_to_hex(hls):
"""
Returns a HLS coordinate into its hex color code equivalent.
:param: hls - An HLS tuple according to the colorsys library (https://docs.python.org/3.8/library/colorsys.html).
:returns: A hex color string equivalent.
"""
h, l, s = hls
r, g, b = hls_to_rgb(h, l, s)
rgb_int = [max(min(int(elem), 255), 0) for elem in [r, g, b]]
return pywal.util.rgb_to_hex(rgb_int)
def hex_to_hls(hex_string):
"""
Returns an HLS coordinate equivalent of the given hex color code.
It uses RGB as the intermediate for converting the hex string.
:param: hex_string - A (hopefully) valid hex string.
:returns: An HLS tuple compatible with the colorsys library.
"""
r, g, b = pywal.util.hex_to_rgb(hex_string)
return rgb_to_hls(r, g, b)
def get_distance(hex_src, hex_tgt):
"""
Returns the distance between two hex values.
The formula used in this function is based from the Wikipedia article (https://en.wikipedia.org/wiki/Color_difference).
:param: hex_src - A hex color string.
:param: hex_tgt - The target hex color code.
:returns: A float that describes the distance.
"""
r1, g1, b1 = pywal.util.hex_to_rgb(hex_src)
r2, g2, b2 = pywal.util.hex_to_rgb(hex_tgt)
return math.sqrt((r2 - r1)**2 + (g2 - g1)**2 + (b2 - b1)**2)
def alter_brightness(hex_string, light, sat=0):
"""
Alters amount of light and saturation in a color.
:param: hex_string - A hex color string (top-notch documentation right here, folks).
:param: light - The amount of light to apply; generally, the acceptable range is -255 to 255 and beyond that is dangerous territory.
:param: sat - The amount of saturation to apply; the acceptable range is -1 to 1.
:returns: A hex color code of the adjusted color.
"""
h, l, s = hex_to_hls(hex_string)
l = max(min(l + light, 255), 1)
s = min(max(s - sat, -1), 0)
return hls_to_hex((h, l, s))
def is_dark_theme(color_list):
"""
Checks by the color list if it's a dark theme.
:param: color_list - A list of hex color codes usually 16 of them.
:returns: A boolean indicating if it's a dark theme.
"""
*_, bg_brightness = hex_to_hls(color_list[0])
*_, fg_brightness = hex_to_hls(color_list[7])
return fg_brightness < bg_brightness
def adjust_colors(colors, light_mode = False):
"""
Create a clear foreground and background set of colors.
:param: colors - A Pywal color object. See https://github.com/dylanaraps/pywal/wiki/Using-%60pywal%60-as-a-module#color-dict-format for more info.
:param: light_mode - Toggle to create a light mode version of the adjustment.
:returns: A Pywal color object with the adjusted values.
"""
colors = smart_sort(colors)
added_sat = 0.25 if light_mode else 0.1
sign = -1 if light_mode else 1
if light_mode == is_dark_theme(colors):
colors[7], colors[0] = colors[0], colors[7]
comment = [alter_brightness(colors[0], sign * 25)]
fg = [alter_brightness(colors[7], sign * 60)]
colors = colors[:8] + comment \
+ [alter_brightness(color, sign * hex_to_hls(color)[1] * 0.3, added_sat) for color in colors[1:7]] + fg
return colors
def smart_sort(colors):
"""
Automatically set the most look-alike colors to their
corresponding place in the standard xterm colors.
:param: colors - A list of hex color strings.
:returns: The color list sorted out.
"""
colors = colors[:8]
sorted_by_color = list()
base_colors = ["#000000", "#ff0000", "#00ff00", "#ffff00",
"#0000ff", "#ff00ff", "#00ffff", "#ffffff"]
for y in base_colors:
cd_tuple = [(x, get_distance(x, y)) for i, x in enumerate(colors)]
cd_tuple.sort(key=itemgetter(1))
sorted_by_color.append(cd_tuple)
i = 0
while i < 8:
current_cd = sorted_by_color[i][0]
closest_cds = [sorted_by_color[x][0] for x in range(8)]
reps = [x for x in range(8) if closest_cds[x][0] == current_cd[0]]
if len(reps) > 1:
closest = min([closest_cds[x] for x in reps], key=itemgetter(1))
reps = [x for x in reps if x != closest_cds.index(closest)]
any(sorted_by_color[x].pop(0) for x in reps)
i = 0
else:
i += 1
sorted_colors = [sorted_by_color[x][0][0] for x in range(8)]
return [*sorted_colors, *sorted_colors]
##################
# MAIN FUNCTIONS #
##################
def setup_args():
"""
Setup the argument parser.
"""
description = "A simple Pywal theme export script with auto-adjusted colors from wpgtk"
argparser = argparse.ArgumentParser(description=description)
argparser.add_argument("input", help="The input (image) of the color scheme to be generated.", metavar="IMAGE")
argparser.add_argument("-o", "--output", help="The location of the colorscheme JSON to be exported.", metavar="FILE")
argparser.add_argument("--no-output", help="Specifies no JSON output to be created; also overrides any output-related options.", action='store_true')
return argparser
def export_wpgtk_colors(image_path):
"""
Export Pywal templates with the given image and the color scheme data.
Take note the exported Pywal object has no 'wallpaper' key for portability.
:param: image_path - The path of the image. ;)
:returns: The Pywal dictionary.
"""
pywal_dict = pywal.colors.get(image_path)
colors = []
for color_name, color in pywal_dict["colors"].items():
colors.append(color)
colors = adjust_colors(colors)
for index, color in enumerate(colors):
pywal_dict["colors"][f"color{index}"] = color
# Export every templates in Pywal including the user templates and reload the newly applied theme.
pywal.export.every(pywal_dict)
del pywal_dict["wallpaper"]
# Feel free to add some reloading code that Pywal provides or something.
# I'm not putting mine since it's intended to only export theme.
# You can't tell me what to do here. >:-)
return pywal_dict
def parse_args(parser, argv):
"""
Parse the args and do the thing.
:param: parser - An `argparse.ArgumentParser` instance.
:param: argv - A list of arguments to be parsed.
"""
args = parser.parse_args(argv)
pywal_dict = export_wpgtk_colors(args.input)
if args.output is not None:
output_file = args.output
else:
output_file = f"{Path(args.input).stem}.json"
if not args.no_output:
# Save the adjusted theme as a JSON file.
with open(output_file, "w") as json_file:
json.dump(pywal_dict, json_file, indent = 4)
if __name__ == "__main__":
argparser = setup_args()
parse_args(argparser, sys.argv[1:])