Merge pull request #52 from dylanaraps/api

api: Start work on a proper api.
This commit is contained in:
Dylan Araps 2017-07-23 15:37:14 +10:00 committed by GitHub
commit 0c1d76e2b3
18 changed files with 305 additions and 234 deletions

5
.pylintrc Normal file
View File

@ -0,0 +1,5 @@
[BASIC]
good-names=i3
[SIMILARITIES]
ignore-imports=yes

View File

@ -11,5 +11,5 @@ install:
script:
- flake8 pywal tests setup.py
- pylint --ignore-imports=yes pywal tests setup.py
- pylint pywal tests setup.py
- python setup.py test

View File

@ -1,5 +1,28 @@
"""
wal - Generate and change colorschemes on the fly.
'||
... ... .... ... ... ... ... .... ||
||' || '|. | || || | '' .|| ||
|| | '|.| ||| ||| .|' || ||
||...' '| | | '|..'|' .||.
|| .. |
'''' ''
Created by Dylan Araps.
"""
from pywal.settings import __version__ # noqa: F401
from .settings import __version__
from . import colors
from . import export
from . import image
from . import reload
from . import sequences
from . import wallpaper
__all__ = [
"__version__",
"colors",
"export",
"image",
"reload",
"sequences",
"wallpaper",
]

View File

@ -1,20 +1,27 @@
"""
wal - Generate and change colorschemes on the fly.
'||
... ... .... ... ... ... ... .... ||
||' || '|. | || || | '' .|| ||
|| | '|.| ||| ||| .|' || ||
||...' '| | | '|..'|' .||.
|| .. |
'''' ''
Created by Dylan Araps.
"""
import argparse
import os
import shutil
import sys
from pywal.settings import CACHE_DIR, __version__
from pywal import export
from pywal import image
from pywal import magic
from pywal import reload
from pywal import sequences
from pywal import util
from pywal import wallpaper
from .settings import __version__, __cache_dir__
from . import colors
from . import export
from . import image
from . import reload
from . import sequences
from . import util
from . import wallpaper
def get_args():
@ -22,7 +29,6 @@ def get_args():
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.")
@ -57,7 +63,6 @@ def get_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.\n"
" Refer to \"wal -h\" for more info.")
@ -68,51 +73,41 @@ def process_args(args):
" Refer to \"wal -h\" for more info.")
exit(1)
# -q
if args.q:
sys.stdout = sys.stderr = open(os.devnull, "w")
# -c
if args.c:
shutil.rmtree(CACHE_DIR / "schemes")
util.create_dir(CACHE_DIR / "schemes")
# -r
if args.r:
sequences.reload_colors(args.t)
# -v
if args.v:
print(f"wal {__version__}")
exit(0)
# -i
if args.q:
sys.stdout = sys.stderr = open(os.devnull, "w")
if args.c:
shutil.rmtree(__cache_dir__ / "schemes", ignore_errors=True)
if args.r:
reload.colors(args.t)
if args.i:
image_file = image.get_image(args.i)
colors_plain = magic.get_colors(image_file, args.q)
image_file = image.get(args.i)
colors_plain = colors.get(image_file, notify=not args.q)
# -f
elif args.f:
colors_plain = util.read_file_json(args.f)
if args.f:
colors_plain = colors.file(args.f)
# -i or -f
if args.i or args.f:
sequences.send_sequences(colors_plain, args.t)
sequences.send(colors_plain, args.t)
if not args.n:
wallpaper.set_wallpaper(colors_plain["wallpaper"])
wallpaper.change(colors_plain["wallpaper"])
export.export_all_templates(colors_plain)
reload.reload_env()
export.every(colors_plain)
reload.env()
# -o
if args.o:
util.disown(args.o)
def main():
"""Main script function."""
util.create_dir(CACHE_DIR / "schemes")
args = get_args()
process_args(args)

View File

@ -5,8 +5,8 @@ import re
import shutil
import subprocess
from pywal.settings import CACHE_DIR, COLOR_COUNT
from pywal import util
from .settings import __cache_dir__, __color_count__
from . import util
def imagemagick(color_count, img):
@ -18,49 +18,64 @@ def imagemagick(color_count, img):
return colors.stdout.readlines()
def gen_colors(img):
def gen_colors(img, color_count):
"""Format the output from imagemagick into a list
of hex colors."""
# Check if the user has Imagemagick installed.
if not shutil.which("convert"):
print("error: imagemagick not found, exiting...\n"
"error: wal requires imagemagick to function.")
exit(1)
# Generate initial scheme.
raw_colors = imagemagick(COLOR_COUNT, img)
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:
while len(raw_colors) - 1 < color_count:
index += 1
raw_colors = imagemagick(COLOR_COUNT + index, img)
raw_colors = imagemagick(color_count + index, img)
print("colors: Imagemagick couldn't generate a", COLOR_COUNT,
print("colors: Imagemagick couldn't generate a", color_count,
"color palette, trying a larger palette size",
COLOR_COUNT + index)
color_count + index)
if index > 20:
print("colors: Imagemagick couldn't generate a suitable scheme",
"for the image. Exiting...")
quit(1)
# Remove the first element, which isn't a color.
# Remove the first element because it isn't a color code.
del raw_colors[0]
# Create a list of hex colors.
return [re.search("#.{6}", str(col)).group(0) for col in raw_colors]
def get_colors(img, quiet):
"""Get the colorscheme."""
# Cache the wallpaper name.
util.save_file(img, CACHE_DIR / "wal")
def sort_colors(img, colors):
"""Sort the generated colors and store them in a dict that
we will later save in json format."""
raw_colors = colors[:1] + colors[9:] + colors[8:]
# Cache the sequences file.
colors = {"wallpaper": img}
colors_special = {}
colors_special.update({"background": raw_colors[0]})
colors_special.update({"foreground": raw_colors[15]})
colors_special.update({"cursor": raw_colors[15]})
colors_hex = {}
for index, color in enumerate(raw_colors):
colors_hex.update({f"color{index}": color})
colors_hex["color8"] = util.set_grey(raw_colors)
colors["special"] = colors_special
colors["colors"] = colors_hex
return colors
def get(img, cache_dir=__cache_dir__,
color_count=__color_count__, notify=False):
"""Get the colorscheme."""
# _home_dylan_img_jpg.json
cache_file = CACHE_DIR / "schemes" / \
cache_file = cache_dir / "schemes" / \
img.replace("/", "_").replace(".", "_")
cache_file = cache_file.with_suffix(".json")
@ -69,48 +84,22 @@ def get_colors(img, quiet):
print("colors: Found cached colorscheme.")
else:
print("colors: Generating a colorscheme...")
if not quiet:
util.disown("notify-send", "wal: Generating a colorscheme...")
util.msg("wal: Generating a colorscheme...", notify)
# Generate the colors.
colors = gen_colors(img)
colors = gen_colors(img, color_count)
colors = sort_colors(img, colors)
# Cache the colorscheme.
util.save_file_json(colors, cache_file)
print("colors: Generated colorscheme")
if not quiet:
util.disown("notify-send", "wal: Generation complete.")
util.msg("wal: Generation complete.", notify)
return colors
def sort_colors(img, colors):
"""Sort the generated colors and store them in a dict that
we will later save in json format."""
raw_colors = colors[:1] + colors[9:] + colors[8:]
def file(input_file):
"""Import colorscheme from json file."""
data = util.read_file_json(input_file)
# Wallpaper.
colors = {"wallpaper": img}
if "wallpaper" not in data:
data["wallpaper"] = "None"
# Special colors.
colors_special = {}
colors_special.update({"background": raw_colors[0]})
colors_special.update({"foreground": raw_colors[15]})
colors_special.update({"cursor": raw_colors[15]})
# Colors 0-15.
colors_hex = {}
[colors_hex.update({f"color{index}": color}) # pylint: disable=W0106
for index, color in enumerate(raw_colors)]
# Color 8.
colors_hex["color8"] = util.set_grey(raw_colors)
# Add the colors to a dict.
colors["special"] = colors_special
colors["colors"] = colors_hex
return colors
return data

View File

@ -2,47 +2,67 @@
Export colors in various formats.
"""
import os
import pathlib
from pywal.settings import CACHE_DIR
from pywal import util
from .settings import __cache_dir__
from . import util
def template(colors, input_file, output_dir):
TEMPLATE_DIR = pathlib.Path(__file__).parent / "templates"
def template(colors, input_file, output_file=None):
"""Read template file, substitute markers and
save the file elsewhere."""
# Import the template.
with open(input_file) as file:
template_data = file.readlines()
# Format the markers.
template_data = util.read_file_raw(input_file)
template_data = "".join(template_data).format(**colors)
# Get the template name.
template_file = os.path.basename(input_file)
# Export the template.
output_file = output_dir / template_file
util.save_file(template_data, output_file)
print(f"export: Exported {template_file}.")
def export_all_templates(colors, template_dir=None, output_dir=CACHE_DIR):
"""Export all template files."""
# Add the template dir to module path.
template_dir = template_dir or \
os.path.join(os.path.dirname(__file__), "templates")
# Merge all colors (specials and normals) into one dict so we can access
# their values simpler.
def flatten_colors(colors):
"""Prepare colors to be exported.
Flatten dicts and convert colors to util.Color()"""
all_colors = {"wallpaper": colors["wallpaper"],
**colors["special"],
**colors["colors"]}
return {k: util.Color(v) for k, v in all_colors.items()}
# Turn all those colors into util.Color instances for accessing the
# .hex and .rgb formats
all_colors = {k: util.Color(v) for k, v in all_colors.items()}
# pylint: disable=W0106
[template(all_colors, file.path, output_dir)
for file in os.scandir(template_dir)]
def get_export_type(export_type):
"""Convert template type to the right filename."""
return {
"css": "colors.css",
"json": "colors.json",
"konsole": "colors-konsole.colorscheme",
"putty": "colors-putty.reg",
"scss": "colors.scss",
"shell": "colors.sh",
"xresources": "colors.Xresources",
}.get(export_type, export_type)
def every(colors, output_dir=__cache_dir__):
"""Export all template files."""
all_colors = flatten_colors(colors)
output_dir = pathlib.Path(output_dir)
for file in os.scandir(TEMPLATE_DIR):
template(all_colors, file.path, output_dir / file.name)
print(f"export: Exported all files.")
def color(colors, export_type, output_file=None):
"""Export a single template file."""
all_colors = flatten_colors(colors)
template_name = get_export_type(export_type)
template_file = TEMPLATE_DIR / template_name
output_file = output_file or __cache_dir__ / template_name
if template_file.is_file():
template(all_colors, template_file, output_file)
print(f"export: Exported {export_type}.")
else:
print(f"[!] warning: template '{export_type}' doesn't exist.")

View File

@ -5,37 +5,33 @@ import os
import pathlib
import random
from pywal.settings import CACHE_DIR
from pywal import util
from .settings import __cache_dir__
from . import util
from . import wallpaper
def get_random_image(img_dir):
"""Pick a random image file from a directory."""
current_wall = CACHE_DIR / "wal"
current_wall = wallpaper.get()
current_wall = os.path.basename(current_wall[0])
if current_wall.is_file():
current_wall = util.read_file(current_wall)
current_wall = os.path.basename(current_wall[0])
# Add all images to a list excluding the current wallpaper.
file_types = (".png", ".jpg", ".jpeg", ".jpe", ".gif")
images = [img for img in os.scandir(img_dir)
if img.name.endswith(file_types) and img.name != current_wall]
# If no images are found, use the current wallpaper.
if not images:
print("image: No new images found (nothing to do), exiting...")
quit(1)
return img_dir / random.choice(images).name
return str(img_dir / random.choice(images).name)
def get_image(img):
def get(img, cache_dir=__cache_dir__):
"""Validate image input."""
image = pathlib.Path(img)
if image.is_file():
wal_img = image
wal_img = str(image)
elif image.is_dir():
wal_img = get_random_image(image)
@ -44,5 +40,8 @@ def get_image(img):
print("error: No valid image file found.")
exit(1)
# Cache the image file path.
util.save_file(wal_img, cache_dir / "wal")
print("image: Using image", wal_img)
return str(wal_img)
return wal_img

View File

@ -1,34 +1,55 @@
"""
Reload programs.
"""
import re
import shutil
import subprocess
from pywal.settings import CACHE_DIR
from pywal import util
from .settings import __cache_dir__
from . import util
def reload_xrdb():
def xrdb(xrdb_file=None):
"""Merge the colors into the X db so new terminals use them."""
xrdb_file = xrdb_file or __cache_dir__ / "colors.Xresources"
if shutil.which("xrdb"):
subprocess.call(["xrdb", "-merge", CACHE_DIR / "colors.Xresources"])
subprocess.call(["xrdb", "-merge", xrdb_file],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL)
def reload_i3():
def i3():
"""Reload i3 colors."""
if shutil.which("i3-msg"):
util.disown("i3-msg", "reload")
def reload_polybar():
def polybar():
"""Reload polybar colors."""
if shutil.which("polybar"):
util.disown("pkill", "-USR1", "polybar")
def reload_env():
def env(xrdb_file=None):
"""Reload environment."""
reload_xrdb()
reload_i3()
reload_polybar()
xrdb(xrdb_file)
i3()
polybar()
print("reload: Reloaded environment.")
def colors(vte, cache_dir=__cache_dir__):
"""Reload the current scheme."""
sequence_file = cache_dir / "sequences"
if sequence_file.is_file():
sequences = "".join(util.read_file(sequence_file))
# If vte mode was used, remove the unsupported sequence.
if vte:
sequences = re.sub(r"\]708;\#.{6}", "", sequences)
print(sequences, end="")
exit(0)

View File

@ -2,10 +2,9 @@
Send sequences to all open terminals.
"""
import os
import re
from pywal.settings import CACHE_DIR
from pywal import util
from .settings import __cache_dir__
from . import util
def set_special(index, color):
@ -18,14 +17,14 @@ def set_color(index, color):
return f"\033]4;{index};{color}\007"
def send_sequences(colors, vte):
def send(colors, vte, cache_dir=__cache_dir__):
"""Send colors to all open terminals."""
# Colors 0-15.
sequences = [set_color(num, color)
for num, color in enumerate(colors["colors"].values())]
# Special colors.
# http://pod.tst.eu/http://cvs.schmorp.de/rxvt-unicode/doc/rxvt.7.pod#XTerm_Operating_System_Commands
# Source: https://goo.gl/KcoQgP
# 10 = foreground, 11 = background, 12 = cursor foregound
# 13 = mouse foreground
sequences.append(set_special(10, colors["special"]["foreground"]))
@ -33,7 +32,7 @@ def send_sequences(colors, vte):
sequences.append(set_special(12, colors["special"]["cursor"]))
sequences.append(set_special(13, colors["special"]["cursor"]))
# Set a blank color that isn"t affected by bold highlighting.
# Set a blank color that isn't affected by bold highlighting.
# Used in wal.vim's airline theme.
sequences.append(set_color(66, colors["special"]["background"]))
@ -41,29 +40,12 @@ def send_sequences(colors, vte):
if not vte:
sequences.append(set_special(708, colors["special"]["background"]))
# Get the list of terminals.
terminals = [f"/dev/pts/{term}" for term in os.listdir("/dev/pts/")
if len(term) < 4]
terminals.append(CACHE_DIR / "sequences")
terminals.append(cache_dir / "sequences")
# Send the sequences to all open terminals.
# pylint: disable=W0106
[util.save_file("".join(sequences), term) for term in terminals]
# Writing to "/dev/pts/[0-9] lets you send data to open terminals.
for term in terminals:
util.save_file("".join(sequences), term)
print("colors: Set terminal colors")
def reload_colors(vte):
"""Reload the current scheme."""
sequence_file = CACHE_DIR / "sequences"
if sequence_file.is_file():
sequences = "".join(util.read_file(sequence_file))
# If vte mode was used, remove the unsupported sequence.
if vte:
sequences = re.sub(r"\]708;\#.{6}", "", sequences)
print(sequences, end="")
exit(0)

View File

@ -1,11 +1,16 @@
"""
Global Constants.
'||
... ... .... ... ... ... ... .... ||
||' || '|. | || || | '' .|| ||
|| | '|.| ||| ||| .|' || ||
||...' '| | | '|..'|' .||.
|| .. |
'''' ''
Created by Dylan Araps.
"""
import pathlib
__version__ = "0.4.0"
COLOR_COUNT = 16
CACHE_DIR = pathlib.Path.home() / ".cache/wal/"
__cache_dir__ = pathlib.Path.home() / ".cache/wal/"
__color_count__ = 16

View File

@ -44,32 +44,40 @@ def set_grey(colors):
def read_file(input_file):
"""Read data from a file."""
with open(input_file) as file:
"""Read data from a file and trim newlines."""
with open(input_file, "r") as file:
data = file.read().splitlines()
return data
def read_file_json(input_file):
"""Read data from a json file."""
with open(input_file) as json_file:
with open(input_file, "r") as json_file:
data = json.load(json_file)
# If wallpaper is unset, set it to "None"
if "wallpaper" not in data:
data["wallpaper"] = "None"
return data
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:
data = file.readlines()
return data
def save_file(data, export_file):
"""Write data to a file."""
create_dir(os.path.dirname(export_file))
with open(export_file, "w") as file:
file.write(data)
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)
@ -98,3 +106,12 @@ def disown(*cmd):
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
preexec_fn=os.setpgrp)
def msg(input_msg, notify):
"""Print to the terminal and display a libnotify
notification."""
if notify:
disown("notify-send", input_msg)
print(input_msg)

View File

@ -3,7 +3,8 @@ import os
import shutil
import subprocess
from pywal import util
from .settings import __cache_dir__
from . import util
def get_desktop_env():
@ -80,7 +81,7 @@ def set_desktop_wallpaper(desktop, img):
set_wm_wallpaper(img)
def set_wallpaper(img):
def change(img):
"""Set the wallpaper."""
if not os.path.isfile(img):
return
@ -94,3 +95,11 @@ def set_wallpaper(img):
set_wm_wallpaper(img)
print("wallpaper: Set the new wallpaper")
def get(cache_dir=__cache_dir__):
"""Get the current wallpaper."""
current_wall = cache_dir / "wal"
if current_wall.is_file():
return util.read_file(current_wall)[0]

20
tests/test_colors.py Normal file → Executable file
View File

@ -1,20 +1,16 @@
"""Test image functions."""
"""Test imagemagick functions."""
import unittest
from pywal import image
from pywal import colors
class TestImage(unittest.TestCase):
"""Test image functions."""
def test_get_img(self):
"""> Validate image file."""
result = image.get_image("tests/test_files/test.jpg")
self.assertEqual(result, "tests/test_files/test.jpg")
class TestGenColors(unittest.TestCase):
"""Test the gen_colors functions."""
def test_get_img_dir(self):
"""> Validate image directory."""
result = image.get_image("tests/test_files")
self.assertEqual(result, "tests/test_files/test.jpg")
def test_gen_colors(self):
"""> Generate a colorscheme."""
result = colors.get("tests/test_files/test.jpg")
self.assertEqual(result["colors"]["color0"], "#0F191A")
if __name__ == "__main__":

View File

@ -8,28 +8,36 @@ from pywal import util
# Import colors.
COLORS = util.read_file_json("tests/test_files/test_file.json")
COLORS["colors"].update(COLORS["special"])
OUTPUT_DIR = pathlib.Path("/tmp/wal")
util.create_dir("/tmp/wal")
class TestExportColors(unittest.TestCase):
"""Test the export functions."""
def test_template(self):
def test_all_templates(self):
"""> Test substitutions in template file."""
# Merge both dicts so we can access their
# values simpler.
COLORS["colors"].update(COLORS["special"])
export.every(COLORS, OUTPUT_DIR)
output_dir = pathlib.Path("/tmp")
template_dir = pathlib.Path("tests/test_files/templates")
export.export_all_templates(COLORS, template_dir, output_dir)
result = pathlib.Path("/tmp/test_template").is_file()
result = pathlib.Path("/tmp/wal/colors.sh").is_file()
self.assertTrue(result)
content = pathlib.Path("/tmp/test_template").read_text()
self.assertEqual(content, '\n'.join(["test1 #1F211E",
"test2 #1F211E",
"test3 31,33,30", ""]))
content = pathlib.Path("/tmp/wal/colors.sh").read_text()
content = content.split("\n")[6]
self.assertEqual(content, "foreground='#F5F1F4'")
def test_css_template(self):
"""> Test substitutions in template file (css)."""
export.color(COLORS, "css", OUTPUT_DIR / "test.css")
result = pathlib.Path("/tmp/wal/test.css").is_file()
self.assertTrue(result)
content = pathlib.Path("/tmp/wal/test.css").read_text()
content = content.split("\n")[6]
self.assertEqual(content, " --background: #1F211E;")
if __name__ == "__main__":

View File

@ -1,3 +0,0 @@
test1 {color0}
test2 {background}
test3 {background.rgb}

1
tests/test_files/test2.jpg Symbolic link
View File

@ -0,0 +1 @@
test.jpg

21
tests/test_image.py Normal file
View File

@ -0,0 +1,21 @@
"""Test image functions."""
import unittest
from pywal import image
class TestImage(unittest.TestCase):
"""Test image functions."""
def test_get_img(self):
"""> Validate image file."""
result = image.get("tests/test_files/test.jpg")
self.assertEqual(result, "tests/test_files/test.jpg")
def test_get_img_dir(self):
"""> Validate image directory."""
result = image.get("tests/test_files")
self.assertEqual(result.endswith(".jpg"), True)
if __name__ == "__main__":
unittest.main()

View File

@ -1,17 +0,0 @@
"""Test imagemagick functions."""
import unittest
from pywal import magic
class TestGenColors(unittest.TestCase):
"""Test the gen_colors functions."""
def test_gen_colors(self):
"""> Generate a colorscheme."""
result = magic.gen_colors("tests/test_files/test.jpg")
self.assertEqual(result[0], "#0F191A")
if __name__ == "__main__":
unittest.main()