#!/usr/bin/env python """ wal - Generate and change colorschemes on the fly. Created by Dylan Araps """ import argparse import glob import os import pathlib import random import re import shutil import subprocess import sys __version__ = "0.1" # Internal variables. COLOR_COUNT = 16 CACHE_DIR = pathlib.Path("%s%s" % (os.path.expanduser("~"), "/.cache/wal/")) class ColorFormats(object): # pylint: disable=too-few-public-methods """Store colors in various formats.""" x_colors = [] sequences = [] # ARGS {{{ def get_args(): """Get the script arguments.""" description = "wal - Generate colorschemes on the fly" arg = argparse.ArgumentParser(description=description) # Add the args. arg.add_argument('-c', action='store_true', help='Delete all cached colorschemes.') arg.add_argument('-i', metavar='"/path/to/img.jpg"', help='Which image or directory to use.') arg.add_argument('-n', action='store_true', help='Skip setting the wallpaper.') arg.add_argument('-o', metavar='"script_name"', help='External script to run after "wal".') arg.add_argument('-q', action='store_true', help='Quiet mode, don\'t print anything.') arg.add_argument('-r', action='store_true', help='Reload current colorscheme.') arg.add_argument('-t', action='store_true', help='Fix artifacts in VTE Terminals. \ (Termite, xfce4-terminal)') 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.") print(" Refer to 'wal -h' for more info.") exit(1) # -q if args.q: sys.stdout = open('/dev/null', 'w') sys.stderr = open('/dev/null', 'w') # -c if args.c: shutil.rmtree(CACHE_DIR / "schemes") # -r if args.r: reload_colors(args.t) # -i if args.i: image = str(get_image(args.i)) # Get the colors. colors = get_colors(image) # Set the wallpaper. if not args.n: set_wallpaper(image) return colors # }}} # 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 elif image.is_dir(): rand = random.choice(os.listdir(image)) rand_img = "%s/%s" % (str(image), rand) rand_img = pathlib.Path(rand_img) if rand_img.is_file(): wal_img = rand_img 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 while len(raw_colors) - 1 <= COLOR_COUNT: 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) # Remove the first element, which isn't a color. del raw_colors[0] # Create a list of hex colors. colors = [re.search('#.{6}', str(col)).group(0) for col in raw_colors] return colors def get_colors(img): """Generate a colorscheme using imagemagick.""" # Cache file. cache_file = CACHE_DIR / "schemes" / img.replace('/', '_') cache_file = pathlib.Path(cache_file) # Cache the wallpaper name. with open(CACHE_DIR / "wal", 'w') as file: file.write("%s\n" % (img)) if cache_file.is_file(): colors = read_colors(cache_file) else: print("colors: Generating a colorscheme...") # Generate the colors. colors = gen_colors(img) colors = sort_colors(colors) # Cache the colorscheme. with open(cache_file, 'w') as file: file.write("\n".join(colors)) 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.""" ColorFormats.sequences.append("\\033]%s;%s\\007" % (str(index), color)) if index == 10: ColorFormats.x_colors.append("URxvt*foreground: %s\n" % (color)) ColorFormats.x_colors.append("XTerm*foreground: %s\n" % (color)) elif index == 11: ColorFormats.x_colors.append("URxvt*background: %s\n" % (color)) ColorFormats.x_colors.append("XTerm*background: %s\n" % (color)) elif index == 12: ColorFormats.x_colors.append("URxvt*cursorColor: %s\n" % (color)) ColorFormats.x_colors.append("XTerm*cursorColor: %s\n" % (color)) def set_color(index, color): """Build the escape sequence we need for each color.""" ColorFormats.x_colors.append("*.color%s: %s\n" % (str(index), color)) ColorFormats.x_colors.append("*color%s: %s\n" % (str(index), color)) ColorFormats.sequences.append("\\033]4;%s;%s\\007" % (str(index), color)) 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]) # This escape sequence doesn't work in VTE terminals. if not vte: set_special(708, colors[0]) # Create the sequences. for num, color in enumerate(colors): set_color(num, color) # Set a blank color that isn't affected by bold highlighting. set_color(66, colors[0]) # Decode the string. sequences = ''.join(ColorFormats.sequences) sequences = bytes(sequences, "utf-8").decode("unicode_escape") # Send the sequences to all open terminals. terminals = glob.glob("/dev/pts/[0-9]*") terminals.append(CACHE_DIR / "sequences") for term in terminals: with open(term, 'w') as file: file.write(sequences) print("colors: Set terminal colors") # }}} # WALLPAPER SETTING {{{ def set_wallpaper(img): """Set the wallpaper.""" uname = os.uname if shutil.which("feh"): subprocess.Popen(["feh", "--bg-fill", img]) elif shutil.which("nitrogen"): subprocess.Popen(["nitrogen", "--set-zoom-fill", img]) elif shutil.which("bgs"): subprocess.Popen(["bgs", img]) elif shutil.which("hsetroot"): subprocess.Popen(["hsetroot", "-fill", img]) elif shutil.which("habak"): subprocess.Popen(["habak", "-mS", img]) elif uname == "Darwin": subprocess.Popen(["osascript", "-e", "'tell application \"Finder\" to set \ desktop picture to POSIX file\'" + img + "\'"]) else: subprocess.Popen(["gsettings", "set", "org.gnome.desktop.background", "picture-uri", img]) print("wallpaper: Set the new wallpaper") return 0 # }}} # EXPORT COLORS {{{ def export_plain(colors): """Export colors to a plain text file.""" with open(CACHE_DIR / "colors", 'w') as file: file.write('\n'.join(colors)) def export_rofi(colors): """Append rofi colors to the x_colors list.""" ColorFormats.x_colors.append("rofi.color-window: %s, %s, %s\n" % (colors[0], colors[0], colors[10])) ColorFormats.x_colors.append("rofi.color-normal: %s, %s, %s, %s, %s\n" % (colors[0], colors[15], colors[0], colors[10], colors[0])) ColorFormats.x_colors.append("rofi.color-active: %s, %s, %s, %s, %s\n" % (colors[0], colors[15], colors[0], colors[10], colors[0])) ColorFormats.x_colors.append("rofi.color-urgent: %s, %s, %s, %s, %s\n" % (colors[0], colors[9], colors[0], colors[9], colors[15])) def export_emacs(colors): """Set emacs colors.""" ColorFormats.x_colors.append("emacs*background: %s\n" % (colors[0])) ColorFormats.x_colors.append("emacs*foreground: %s\n" % (colors[15])) def export_xrdb(colors): """Export colors to xrdb.""" x_colors = ''.join(colors) # Write the colors to the file. with open(CACHE_DIR / "xcolors", 'w') as file: file.write(x_colors) # Merge the colors into the X db so new terminals use them. subprocess.Popen(["xrdb", "-merge", CACHE_DIR / "xcolors"]) print("export: Exported xrdb colors.") def export_colors(colors): """Export colors in various formats.""" export_plain(colors) # X based colors. export_rofi(colors) export_emacs(colors) export_xrdb(ColorFormats.x_colors) # }}} # OTHER FUNCTIONS {{{ def read_colors(color_file): """Read colors from a file""" with open(color_file) as file: colors = file.readlines() # Strip newlines from each list element. colors = [x.strip() for x in colors] return colors def reload_colors(vte): """Reload colors.""" with open(CACHE_DIR / "sequences") as file: sequences = file.read() # If vte mode was used, remove the problem sequence. if vte: sequences = re.sub(r'\]708;\#.{6}', '', sequences) # Decode the string. sequences = bytes(sequences, "utf-8").decode("unicode_escape") print(sequences, end='') quit() # }}} def main(): """Main script function.""" # Create colorscheme dir. pathlib.Path(CACHE_DIR / "schemes").mkdir(parents=True, exist_ok=True) # Get the args. args = get_args() colors = process_args(args) # Set the colors. send_sequences(colors, args.t) export_colors(colors) # -o if args.o: subprocess.Popen(["nohup", args.o], stdout=open('/dev/null', 'w'), stderr=open('/dev/null', 'w'), preexec_fn=os.setpgrp) return 0 main()