pywal/wal

563 lines
15 KiB
Plaintext
Raw Normal View History

2017-06-20 06:23:13 +02:00
#!/usr/bin/env python
"""
wal - Generate and change colorschemes on the fly.
Created by Dylan Araps
"""
import argparse
import os
import pathlib
import random
import re
import shutil
import subprocess
import sys
__version__ = "0.1"
# Internal variables.
COLOR_COUNT = 16
2017-06-20 06:43:41 +02:00
CACHE_DIR = pathlib.Path.home() / ".cache/wal/"
2017-06-20 06:23:13 +02:00
2017-06-20 12:19:24 +02:00
# pylint: disable=too-few-public-methods
class ColorType(object):
2017-06-20 06:23:13 +02:00
"""Store colors in various formats."""
2017-06-22 02:45:39 +02:00
plain = []
xrdb = []
2017-06-20 06:23:13 +02:00
sequences = []
shell = []
scss = []
css = [":root {"]
putty = [
"Windows Registry Editor Version 5.00",
"[HKEY_CURRENT_USER\\Software\\SimonTatham\\PuTTY\\Sessions\\Wal]",
]
2017-06-20 06:23:13 +02:00
# ARGS {{{
def get_args():
"""Get the script arguments."""
description = "wal - Generate colorschemes on the fly"
arg = argparse.ArgumentParser(description=description)
# Add the args.
2017-06-20 08:00:00 +02:00
arg.add_argument("-c", action="store_true",
help="Delete all cached colorschemes.")
2017-06-20 06:23:13 +02:00
2017-06-20 08:00:00 +02:00
arg.add_argument("-i", metavar="\"/path/to/img.jpg\"",
help="Which image or directory to use.")
2017-06-20 06:23:13 +02:00
2017-06-20 08:00:00 +02:00
arg.add_argument("-n", action="store_true",
help="Skip setting the wallpaper.")
2017-06-20 06:23:13 +02:00
2017-06-20 08:00:00 +02:00
arg.add_argument("-o", metavar="\"script_name\"",
help="External script to run after \"wal\".")
2017-06-20 06:23:13 +02:00
2017-06-20 08:00:00 +02:00
arg.add_argument("-q", action="store_true",
help="Quiet mode, don\"t print anything.")
2017-06-20 06:23:13 +02:00
2017-06-20 08:00:00 +02:00
arg.add_argument("-r", action="store_true",
help="Reload current colorscheme.")
2017-06-20 06:23:13 +02:00
2017-06-20 08:00:00 +02:00
arg.add_argument("-t", action="store_true",
help="Fix artifacts in VTE Terminals. \
(Termite, xfce4-terminal)")
2017-06-20 06:23:13 +02:00
2017-06-22 03:02:51 +02:00
arg.add_argument("-v", action="store_true",
help="Print \"wal\" version.")
2017-06-20 06:23:13 +02:00
return arg.parse_args()
def process_args(args):
"""Process args"""
# If no args were passed.
if not len(sys.argv) > 1:
print("error: wal needs to be given arguments to run.")
2017-06-20 08:00:00 +02:00
print(" Refer to \"wal -h\" for more info.")
2017-06-20 06:23:13 +02:00
exit(1)
# -q
if args.q:
sys.stdout = sys.stderr = open("/dev/null", "w")
2017-06-20 06:23:13 +02:00
# -c
if args.c:
shutil.rmtree(CACHE_DIR / "schemes")
2017-06-20 06:43:41 +02:00
create_cache_dir()
2017-06-20 06:23:13 +02:00
# -r
if args.r:
reload_colors(args.t)
2017-06-22 03:02:51 +02:00
# -v
if args.v:
print(f"wal {__version__}")
2017-06-22 03:02:51 +02:00
exit(0)
2017-06-20 06:23:13 +02:00
# -i
if args.i:
image = str(get_image(args.i))
ColorType.plain = get_colors(image)
2017-06-21 14:40:51 +02:00
2017-06-20 06:23:13 +02:00
if not args.n:
set_wallpaper(image)
# Set the colors.
send_sequences(ColorType.plain, args.t)
export_colors(ColorType.plain)
# -o
if args.o:
subprocess.Popen(["nohup", args.o],
2017-06-22 03:56:23 +02:00
stdout=open("/dev/null", "w"),
stderr=open("/dev/null", "w"),
preexec_fn=os.setpgrp)
2017-06-20 06:23:13 +02:00
# }}}
# COLORSCHEME GENERATION {{{
def get_image(img):
"""Validate image input."""
image = pathlib.Path(img)
# Check if the user has Imagemagick installed.
if not shutil.which("convert"):
print("error: imagemagick not found, exiting...")
print("error: wal requires imagemagick to function.")
exit(1)
if image.is_file():
wal_img = image
# Pick a random image from the directory.
2017-06-20 06:23:13 +02:00
elif image.is_dir():
2017-06-20 11:57:24 +02:00
file_types = (".png", ".jpg", ".jpeg", ".jpe", ".gif")
# Get the filename of the current wallpaper.
current_img = pathlib.Path(CACHE_DIR / "wal")
if current_img.is_file():
current_img = read_file(current_img)
current_img = os.path.basename(current_img[0])
# Get a list of images.
2017-06-20 11:57:24 +02:00
images = [img for img in os.listdir(image)
if img.endswith(file_types) and
img != current_img]
rand_img = random.choice(images)
2017-06-20 11:57:24 +02:00
rand_img = pathlib.Path(image / rand_img)
2017-06-20 06:23:13 +02:00
if rand_img.is_file():
wal_img = rand_img
else:
print("error: No valid image file found.")
exit(1)
2017-06-20 06:23:13 +02:00
print("image: Using image", wal_img)
return wal_img
def imagemagick(color_count, img):
"""Call Imagemagick to generate a scheme."""
colors = subprocess.Popen(["convert", img, "+dither", "-colors",
str(color_count), "-unique-colors", "txt:-"],
stdout=subprocess.PIPE)
return colors.stdout.readlines()
def gen_colors(img):
"""Generate a color palette using imagemagick."""
# Generate initial scheme.
raw_colors = imagemagick(COLOR_COUNT, img)
# If imagemagick finds less than 16 colors, use a larger source number
# of colors.
index = 0
2017-06-20 07:06:55 +02:00
while len(raw_colors) - 1 < COLOR_COUNT:
2017-06-20 06:23:13 +02:00
index += 1
raw_colors = imagemagick(COLOR_COUNT + index, img)
print("colors: Imagemagick couldn't generate a", COLOR_COUNT,
"color palette, trying a larger palette size",
COLOR_COUNT + index)
2017-06-20 08:00:00 +02:00
# Remove the first element, which isn"t a color.
2017-06-20 06:23:13 +02:00
del raw_colors[0]
# Create a list of hex colors.
2017-06-20 08:00:00 +02:00
colors = [re.search("#.{6}", str(col)).group(0) for col in raw_colors]
2017-06-20 06:23:13 +02:00
return colors
def get_colors(img):
"""Generate a colorscheme using imagemagick."""
# Cache the wallpaper name.
save_file(img, CACHE_DIR / "wal")
# Cache the sequences file.
2017-06-20 08:00:00 +02:00
cache_file = CACHE_DIR / "schemes" / img.replace("/", "_")
2017-06-20 06:23:13 +02:00
cache_file = pathlib.Path(cache_file)
if cache_file.is_file():
colors = read_file(cache_file)
2017-06-20 06:23:13 +02:00
else:
print("colors: Generating a colorscheme...")
# Generate the colors.
colors = gen_colors(img)
colors = sort_colors(colors)
# Cache the colorscheme.
save_file("\n".join(colors), cache_file)
2017-06-20 06:23:13 +02:00
print("colors: Generated colorscheme")
return colors
def sort_colors(colors):
"""Sort the generated colors."""
sorted_colors = []
sorted_colors.append(colors[0])
sorted_colors.append(colors[9])
sorted_colors.append(colors[10])
sorted_colors.append(colors[11])
sorted_colors.append(colors[12])
sorted_colors.append(colors[13])
sorted_colors.append(colors[14])
sorted_colors.append(colors[15])
sorted_colors.append(set_grey(colors))
sorted_colors.append(colors[9])
sorted_colors.append(colors[10])
sorted_colors.append(colors[11])
sorted_colors.append(colors[12])
sorted_colors.append(colors[13])
sorted_colors.append(colors[14])
sorted_colors.append(colors[15])
return sorted_colors
# }}}
# SEND SEQUENCES {{{
def set_special(index, color):
"""Build the escape sequence for special colors."""
ColorType.sequences.append(f"\\033]{index};{color}\\007")
2017-06-20 06:23:13 +02:00
if index == 10:
ColorType.xrdb.append(f"URxvt*foreground: {color}")
ColorType.xrdb.append(f"XTerm*foreground: {color}")
2017-06-20 06:23:13 +02:00
elif index == 11:
ColorType.xrdb.append(f"URxvt*background: {color}")
ColorType.xrdb.append(f"XTerm*background: {color}")
2017-06-20 06:23:13 +02:00
elif index == 12:
ColorType.xrdb.append(f"URxvt*cursorColor: {color}")
ColorType.xrdb.append(f"XTerm*cursorColor: {color}")
2017-06-20 06:23:13 +02:00
def set_color(index, color):
"""Build the escape sequence we need for each color."""
ColorType.xrdb.append(f"*.color{index}: {color}")
ColorType.xrdb.append(f"*color{index}: {color}")
ColorType.sequences.append(f"\\033]4;{index};{color}\\007")
ColorType.shell.append(f"color{index}='{color}'")
ColorType.css.append(f"\t--color{index}: {color};")
ColorType.scss.append(f"$color{index}: {color};")
rgb = hex_to_rgb(color)
ColorType.putty.append(f"\"Colour{index}\"=\"{rgb}\"")
2017-06-20 06:23:13 +02:00
def set_grey(colors):
"""Set a grey color based on brightness of color0"""
return {
0: "#666666",
1: "#666666",
2: "#757575",
3: "#999999",
4: "#999999",
5: "#8a8a8a",
6: "#a1a1a1",
7: "#a1a1a1",
8: "#a1a1a1",
9: "#a1a1a1",
}.get(int(colors[0][1]), colors[7])
def send_sequences(colors, vte):
"""Send colors to all open terminals."""
set_special(10, colors[15])
set_special(11, colors[0])
set_special(12, colors[15])
set_special(13, colors[15])
set_special(14, colors[0])
2017-06-20 08:00:00 +02:00
# This escape sequence doesn"t work in VTE terminals.
2017-06-20 06:23:13 +02:00
if not vte:
set_special(708, colors[0])
# Create the sequences.
2017-06-20 12:19:24 +02:00
# pylint: disable=W0106
[set_color(num, color) for num, color in enumerate(colors)]
2017-06-20 06:23:13 +02:00
2017-06-20 08:00:00 +02:00
# Set a blank color that isn"t affected by bold highlighting.
2017-06-20 06:23:13 +02:00
set_color(66, colors[0])
# Make the terminal interpret escape sequences.
sequences = "".join(ColorType.sequences)
sequences = fix_escape(sequences)
2017-06-20 06:23:13 +02:00
2017-06-20 12:19:24 +02:00
# Get a list of terminals.
terminals = [f"/dev/pts/{term}" for term in os.listdir("/dev/pts/")
if len(term) < 4]
2017-06-20 06:23:13 +02:00
terminals.append(CACHE_DIR / "sequences")
2017-06-20 12:19:24 +02:00
# Send the sequences to all open terminals.
# pylint: disable=W0106
[save_file(sequences, term) for term in terminals]
2017-06-20 06:23:13 +02:00
print("colors: Set terminal colors")
# }}}
# WALLPAPER SETTING {{{
def get_desktop_env():
"""Identify the current running desktop environment."""
desktop = os.getenv("XDG_CURRENT_DESKTOP")
if desktop:
return desktop
desktop = os.getenv("DESKTOP_SESSION")
if desktop:
return desktop
desktop = os.getenv("GNOME_DESKTOP_SESSION_ID")
if desktop:
return "GNOME"
desktop = os.getenv("MATE_DESKTOP_SESSION_ID")
if desktop:
return "MATE"
def set_desktop_wallpaper(desktop, img):
"""Set the wallpaper for the desktop environment."""
2017-06-20 15:16:49 +02:00
desktop = str(desktop).lower()
if "xfce" in desktop or "xubuntu" in desktop:
subprocess.Popen(["xfconf-query", "--channel", "xfce4-desktop",
"--property",
"/backdrop/screen0/monitor0/image-path",
"--set", img])
# XFCE requires two commands since they differ between versions.
subprocess.Popen(["xfconf-query", "--channel", "xfce4-desktop",
"--property",
"/backdrop/screen0/monitor0/workspace0/last-image",
"--set", img])
elif "muffin" in desktop or "cinnamon" in desktop:
subprocess.Popen(["gsettings", "set",
"org.cinnamon.desktop.background",
"picture-uri", "file:///" + img])
elif "mate" in desktop:
subprocess.Popen(["gsettings", "set", "org.mate.background",
"picture-filename", img])
elif "gnome" in desktop:
subprocess.Popen(["gsettings", "set",
"org.gnome.desktop.background",
"picture-uri", "file:///" + img])
2017-06-20 06:23:13 +02:00
def set_wallpaper(img):
"""Set the wallpaper."""
2017-06-20 15:16:49 +02:00
desktop = get_desktop_env()
2017-06-20 06:23:13 +02:00
if desktop:
set_desktop_wallpaper(desktop, img)
2017-06-20 06:23:13 +02:00
else:
if os.uname == "Darwin":
subprocess.Popen(["osascript", "-e", "\"tell application \"Finder\" to set \
desktop picture to POSIX file\"" + img + "\""])
2017-06-20 06:23:13 +02:00
elif shutil.which("feh"):
subprocess.Popen(["feh", "--bg-fill", img])
2017-06-20 06:23:13 +02:00
elif shutil.which("nitrogen"):
subprocess.Popen(["nitrogen", "--set-zoom-fill", img])
2017-06-20 06:23:13 +02:00
elif shutil.which("bgs"):
subprocess.Popen(["bgs", img])
2017-06-20 06:23:13 +02:00
elif shutil.which("hsetroot"):
subprocess.Popen(["hsetroot", "-fill", img])
2017-06-20 06:23:13 +02:00
elif shutil.which("habak"):
subprocess.Popen(["habak", "-mS", img])
else:
print("error: No wallpaper setter found.")
return
2017-06-20 06:23:13 +02:00
print("wallpaper: Set the new wallpaper")
return 0
# }}}
# EXPORT COLORS {{{
def save_colors(colors, export_file, message):
2017-06-20 06:36:00 +02:00
"""Export colors to var format."""
colors = "\n".join(colors)
save_file(f"{colors}\n", export_file)
print(f"export: exported {message}.")
2017-06-20 06:36:00 +02:00
2017-06-20 06:23:13 +02:00
def export_rofi(colors):
"""Append rofi colors to the x_colors list."""
ColorType.xrdb.append(f"rofi.color-window: {colors[0]}, \
{colors[0]}, {colors[10]}")
ColorType.xrdb.append(f"rofi.color-normal: {colors[0]}, \
{colors[15]}, {colors[0]}, \
{colors[10]}, {colors[0]}")
ColorType.xrdb.append(f"rofi.color-active: {colors[0]}, \
{colors[15]}, {colors[0]}, \
{colors[10]}, {colors[0]}")
ColorType.xrdb.append(f"rofi.color-urgent: {colors[0]}, \
{colors[9]}, {colors[0]}, \
{colors[9]}, {colors[15]}")
2017-06-20 06:23:13 +02:00
def export_emacs(colors):
"""Set emacs colors."""
ColorType.xrdb.append(f"emacs*background: {colors[0]}")
ColorType.xrdb.append(f"emacs*foreground: {colors[15]}")
2017-06-20 06:23:13 +02:00
2017-06-20 06:36:00 +02:00
def export_xrdb(colors, export_file):
2017-06-20 06:23:13 +02:00
"""Export colors to xrdb."""
save_colors(colors, export_file, "xrdb colors")
2017-06-20 06:23:13 +02:00
# Merge the colors into the X db so new terminals use them.
2017-06-21 11:48:04 +02:00
subprocess.call(["xrdb", "-merge", export_file])
2017-06-20 07:18:57 +02:00
2017-06-20 06:23:13 +02:00
def export_colors(colors):
"""Export colors in various formats."""
2017-06-22 02:45:39 +02:00
save_colors(ColorType.plain, CACHE_DIR / "colors", "plain hex colors")
save_colors(ColorType.shell, CACHE_DIR / "colors.sh", "shell variables")
2017-06-20 06:23:13 +02:00
2017-06-20 06:36:00 +02:00
# Web based colors.
ColorType.css.append("}")
save_colors(ColorType.css, CACHE_DIR / "colors.css", "css variables")
save_colors(ColorType.scss, CACHE_DIR / "colors.scss", "scss variables")
2017-06-20 06:23:13 +02:00
2017-06-20 07:18:57 +02:00
# Text editor based colors.
save_colors(ColorType.putty, CACHE_DIR / "colors-putty.reg", "putty theme")
2017-06-20 07:18:57 +02:00
2017-06-21 11:48:04 +02:00
# X based colors.
export_rofi(colors)
export_emacs(colors)
export_xrdb(ColorType.xrdb, CACHE_DIR / "xcolors")
2017-06-20 06:23:13 +02:00
# }}}
# OTHER FUNCTIONS {{{
def reload_colors(vte):
"""Reload colors."""
2017-06-21 14:32:34 +02:00
sequence_file = pathlib.Path(CACHE_DIR / "sequences")
2017-06-20 06:23:13 +02:00
2017-06-21 14:32:34 +02:00
if sequence_file.is_file():
with open(sequence_file, "r") as file:
sequences = file.read()
2017-06-20 06:23:13 +02:00
2017-06-21 14:32:34 +02:00
# If vte mode was used, remove the problem sequence.
if vte:
sequences = re.sub(r"\]708;\#.{6}", "", sequences)
# Make the terminal interpret escape sequences.
sequences = fix_escape(sequences)
2017-06-21 14:32:34 +02:00
print(sequences, end="")
2017-06-20 06:23:13 +02:00
2017-06-22 03:02:51 +02:00
exit(0)
2017-06-20 06:23:13 +02:00
def read_file(input_file):
"""Read colors from a file"""
with open(input_file) as file:
contents = file.readlines()
# Strip newlines from each list element.
contents = [x.strip() for x in contents]
return contents
2017-06-20 06:36:00 +02:00
def save_file(colors, export_file):
"""Write the colors to the file."""
2017-06-20 08:00:00 +02:00
with open(export_file, "w") as file:
2017-06-20 06:36:00 +02:00
file.write(colors)
2017-06-20 06:43:41 +02:00
def create_cache_dir():
"""Alias to create the cache dir."""
pathlib.Path(CACHE_DIR / "schemes").mkdir(parents=True, exist_ok=True)
2017-06-20 07:18:57 +02:00
def hex_to_rgb(color):
"""Convert a hex color to rgb."""
red, green, blue = list(bytes.fromhex(color.strip("#")))
return f"{red},{green},{blue}"
2017-06-20 07:18:57 +02:00
def fix_escape(string):
"""Decode a string."""
return bytes(string, "utf-8").decode("unicode_escape")
2017-06-20 06:23:13 +02:00
# }}}
def main():
"""Main script function."""
2017-06-20 06:43:41 +02:00
create_cache_dir()
2017-06-20 06:23:13 +02:00
args = get_args()
process_args(args)
2017-06-20 06:23:13 +02:00
# This saves 10ms.
# pylint: disable=W0212
2017-06-22 02:58:17 +02:00
# os._exit(0)
2017-06-20 06:23:13 +02:00
main()