-
-
Save coleifer/5071da9c63a321c5da86 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
variables: | |
primary: magenta | |
secondary: green | |
tertiary: red | |
special: yellow | |
transparency: 85 | |
fontName: terminus | |
fontSize: 12 | |
font: "-*-terminus-*-*-*-*-12-*-*-*-*-*-*-*" | |
files: | |
xresources.tpl: Xresources |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python | |
import logging | |
import optparse | |
import os | |
import re | |
import shutil | |
import sys | |
import urllib2 | |
import yaml | |
from jinja2 import Environment, FileSystemLoader | |
logger = logging.getLogger(__name__) | |
DEFAULT_CONTEXT_CONFIG = { | |
'primary': 'magenta', | |
'secondary': 'green', | |
'tertiary': 'blue', | |
} | |
THEMER_ROOT = os.path.join(os.environ['HOME'], '.themer') | |
TEMPLATE_ROOT = os.path.join(THEMER_ROOT, 'templates') | |
def dict_update(parent, child): | |
"""Recursively update parent dict with child dict.""" | |
for key, value in child.iteritems(): | |
if key in parent and isinstance(parent[key], dict): | |
parent[key] = dict_update(parent[key], value) | |
else: | |
parent[key] = value | |
return parent | |
def read_config(config_file): | |
"""Read a YAML config file.""" | |
logger.debug('Reading config file: %s' % config_file) | |
config_dir = os.path.dirname(config_file) | |
base_config = {} | |
with open(config_file) as fh: | |
data = yaml.load(fh) | |
if data.get('extends'): | |
parent_config = os.path.join(config_dir, data['extends']) | |
base_config = read_config(parent_config) | |
return dict_update(base_config, data) | |
def render_templates(template_dir, files, context): | |
"""Render templates from `template_dir`.""" | |
env = Environment(loader=FileSystemLoader(template_dir)) | |
logger.debug('Jinja environment configured for: %s' % template_dir) | |
for src, dest in files.items(): | |
dir_name = os.path.dirname(dest) | |
if not os.path.exists(dir_name): | |
logger.debug('Creating directory %s' % dir_name) | |
os.makedirs(dir_name) | |
if src.endswith(('tpl', 'conf')): | |
logger.info('Writing %s -> %s' % (src, dest)) | |
template = env.get_template(src) | |
with open(dest, 'w') as fh: | |
fh.write(template.render(**context).encode('utf-8')) | |
else: | |
logger.info('Copying %s -> %s' % (src, dest)) | |
shutil.copy(os.path.join(template_dir, src), dest) | |
def munge_context(variables, colors): | |
context = {} | |
context.update(variables) | |
# Handle case when a variable may reference a color, e.g. | |
# `primary` = `alt_red`, then `primary` = `#fd1a2b` | |
for key, value in context.items(): | |
if value in colors: | |
context[key] = colors[value] | |
context.update(colors) | |
for key, value in DEFAULT_CONTEXT_CONFIG.items(): | |
if key not in context: | |
context[key] = context[value] | |
return context | |
def wallfix(directory): | |
"""Look in `directory` for file named `wallpaper.xxx` and set it.""" | |
for filename in os.listdir(directory): | |
if filename.startswith('wallpaper'): | |
logger.info('Setting %s as wallpaper' % filename) | |
os.system('wallfix %s' % os.path.join(directory, filename)) | |
return | |
logger.info('No wallpaper found, skipping.') | |
def symlink(theme_name): | |
"""Set up a symlink for the new theme.""" | |
logger.info('Setting %s as current theme' % theme_name) | |
current = os.path.join(THEMER_ROOT, 'current') | |
if os.path.islink(current): | |
os.unlink(current) | |
os.symlink(os.path.join(THEMER_ROOT, theme_name), current) | |
def activate(theme_name): | |
"""Activate the given theme.""" | |
symlink(theme_name) | |
dest = os.path.join(THEMER_ROOT, theme_name) | |
wallfix(dest) | |
color_file = os.path.join(dest, 'colors.yaml') | |
colors = CachedColorParser(color_file).read() | |
IconUpdater(colors['primary'], colors['secondary']).update_icons() | |
os.system('xrdb -merge ~/.Xresources') | |
os.system('i3-msg -q restart') | |
def fetch_vim(color_file): | |
return urllib2.urlopen('http://sweyla.com/themes/vim/sweyla%s.vim' % color_file).read() | |
def generate(color_source, config_file, template_dir, theme_name): | |
"""Generate a new theme.""" | |
destination = os.path.join(THEMER_ROOT, theme_name) | |
if color_source.isdigit() and not os.path.isfile(color_source): | |
colors = SweylaColorParser(color_source).read() | |
vim = fetch_vim(color_source) | |
else: | |
colors = ColorParser(color_source).read() | |
vim = None | |
config = read_config(config_file) | |
context = munge_context(config['variables'], colors) | |
files = { | |
key: os.path.join(destination, value) | |
for key, value in config['files'].items()} | |
render_templates(template_dir, files, context) | |
# Save a copy of the colors in the generated theme folder. | |
with open(os.path.join(destination, 'colors.yaml'), 'w') as fh: | |
yaml.dump(context, fh, default_flow_style=False) | |
# Save the vim color scheme. | |
if vim: | |
logger.info('Saving vim colorscheme %s.vim' % theme_name) | |
filename = os.path.join(os.environ['HOME'], '.vim/colors/%s.vim' % theme_name) | |
with open(filename, 'w') as fh: | |
fh.write(vim) | |
class ColorParser(object): | |
# Colors look something like "*color0: #FF0d3c\n" | |
color_re = re.compile('.*?(color[^:]+|background|foreground):\s*(#[\da-z]{6})') | |
def __init__(self, color_file): | |
self.color_file = color_file | |
self.colors = {} | |
def mapping(self): | |
return { | |
'background': 'background', | |
'foreground': 'foreground', | |
'color0': 'black', | |
'color8': 'alt_black', | |
'color1': 'red', | |
'color9': 'alt_red', | |
'color2': 'green', | |
'color10': 'alt_green', | |
'color3': 'yellow', | |
'color11': 'alt_yellow', | |
'color4': 'blue', | |
'color12': 'alt_blue', | |
'color5': 'magenta', | |
'color13': 'alt_magenta', | |
'color6': 'cyan', | |
'color14': 'alt_cyan', | |
'color7': 'white', | |
'color15': 'alt_white', | |
'colorul': 'underline'} | |
def read(self): | |
color_mapping = self.mapping() | |
with open(self.color_file) as fh: | |
for line in fh.readlines(): | |
if line.startswith('!'): | |
continue | |
match_obj = self.color_re.search(line.lower()) | |
if match_obj: | |
var, color = match_obj.groups() | |
self.colors[color_mapping[var]] = color | |
if len(self.colors) < 16: | |
logger.warning( | |
'Error, only %s colors were read when loading color file "%s"' | |
% (len(self.colors), self.color_file)) | |
return self.colors | |
class SweylaColorParser(ColorParser): | |
def mapping(self): | |
return { | |
'bg': ['background', 'black', 'alt_black'], | |
'fg': ['foreground', 'white'], | |
'nf': 'red', # name of function / method | |
'nd': 'alt_red', # decorator | |
'nc': 'green', # name of class | |
'nt': 'alt_green', # ??? | |
'nb': 'yellow', # e.g., "object" or "open" | |
'c': 'alt_yellow', # comments | |
's': 'blue', # string | |
'mi': 'alt_blue', # e.g., a number | |
'k': 'magenta', # e.g., "class" | |
'o': 'alt_magenta', # operator, e.g "=" | |
'bp': 'cyan', # e.g., "self" keyword | |
'si': 'alt_cyan', # e.g. "%d" | |
'se': 'alt_white', | |
'support_function': 'underline'} | |
def read(self): | |
mapping = self.mapping() | |
resp = urllib2.urlopen( | |
'http://sweyla.com/themes/textfile/sweyla%s.txt' % self.color_file) | |
contents = resp.read() | |
for line in contents.splitlines(): | |
key, value = line.split(':\t') | |
if key in mapping: | |
colors = mapping[key] | |
if not isinstance(colors, list): | |
colors = [colors] | |
for color in colors: | |
self.colors[color] = value | |
return self.colors | |
class CachedColorParser(ColorParser): | |
def read(self): | |
with open(self.color_file) as fh: | |
self.colors = yaml.load(fh) | |
return self.colors | |
class IconUpdater(object): | |
def __init__(self, primary_color, secondary_color): | |
self.primary_color = primary_color | |
self.secondary_color = secondary_color | |
def icon_path(self): | |
return os.path.join(os.environ['HOME'], '.icons/acyl') | |
def primary_icon(self): | |
return os.path.join(self.icon_path(), 'scalable/places/desktop.svg') | |
def secondary_icon(self): | |
return os.path.join(self.icon_path(), 'scalable/actions/add.svg') | |
def extract_color_svg(self, filename): | |
regex = re.compile('stop-color:(#[\da-zA-Z]{6})') | |
with open(filename, 'r') as fh: | |
for line in fh.readlines(): | |
match_obj = regex.search(line) | |
if match_obj: | |
return match_obj.groups()[0] | |
raise ValueError('Unable to determine icon color.') | |
def update_icons(self): | |
# Introspect a couple icon files to determine what colors are being used | |
# currently. | |
old_primary = self.extract_color_svg(self.primary_icon()) | |
old_secondary = self.extract_color_svg(self.secondary_icon()) | |
logger.debug('Old icon colors: %s, %s' % (old_primary, old_secondary)) | |
# Walk the icons, updating the colors in each svg file. | |
file_count = 0 | |
for root, dirs, filenames in os.walk(self.icon_path()): | |
for filename in filenames: | |
if not filename.endswith('.svg'): | |
continue | |
path = os.path.join(root, filename) | |
with open(path, 'r+') as fh: | |
contents = fh.read() | |
contents = contents.replace(old_primary, self.primary_color) | |
contents = contents.replace(old_secondary, self.secondary_color) | |
fh.seek(0) | |
fh.write(contents) | |
file_count += 1 | |
logger.info('Checked %d icon files' % file_count) | |
def get_parser(): | |
parser = optparse.OptionParser(usage='usage: %prog [options] [list|activate|generate|current|delete] theme_name [color file]') | |
parser.add_option('-t', '--template', dest='template_dir', default='i3') | |
parser.add_option('-c', '--config', dest='config_file', default='config.yaml') | |
parser.add_option('-a', '--activate', dest='activate', action='store_true') | |
parser.add_option('-v', '--verbose', dest='verbose', action='store_true') | |
parser.add_option('-d', '--debug', dest='debug', action='store_true') | |
return parser | |
def panic(msg): | |
print >> sys.stderr, msg | |
sys.exit(1) | |
if __name__ == '__main__': | |
parser = get_parser() | |
options, args = parser.parse_args() | |
if not args: | |
panic(parser.get_usage()) | |
action = args[0] | |
if action not in ('list', 'activate', 'generate', 'current', 'delete'): | |
panic('Unknown action "%s"' % action) | |
if action not in ('list', 'current') and len(args) == 1: | |
panic('Missing required argument "theme_name"') | |
elif action == 'list': | |
themes = [ | |
t for t in os.listdir(THEMER_ROOT) | |
if t not in ('templates', 'current')] | |
print '\n'.join(sorted(themes)) | |
sys.exit(0) | |
elif action == 'current': | |
current = os.path.join(THEMER_ROOT, 'current') | |
if not os.path.exists(current): | |
print 'No theme' | |
else: | |
print os.path.basename(os.path.realpath( | |
os.path.join(THEMER_ROOT, 'current'))) | |
os.system('colortheme') | |
sys.exit(0) | |
theme_name = args[1] | |
# Add logging handlers. | |
if options.verbose or options.debug: | |
handler = logging.StreamHandler() | |
logger.addHandler(handler) | |
if options.debug: | |
logger.setLevel(logging.DEBUG) | |
if action == 'activate': | |
activate(theme_name) | |
elif action == 'delete': | |
shutil.rmtree(os.path.join(THEMER_ROOT, theme_name)) | |
logger.info('Removed %s' % theme_name) | |
else: | |
# Find the appropriate yaml config file and load it. | |
template_dir = os.path.join(TEMPLATE_ROOT, options.template_dir) | |
config_file = os.path.join(template_dir, options.config_file) | |
if not os.path.exists(config_file): | |
panic('Unable to find file "%s"' % config_file) | |
if not len(args) == 3: | |
panic('Missing required color file') | |
else: | |
color_file = args[2] | |
generate(color_file, config_file, template_dir, theme_name) | |
if options.activate or raw_input('Activate now? yN ') == 'y': | |
activate(theme_name) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
! {{ name }} colors | |
*background: {% if background %}{{ background }}{% else %}{{ black }}{% endif %} | |
*foreground: {% if foreground %}{{ foreground }}{% else %}{{ white }}{% endif %} | |
! black | |
*color0: {{ black }} | |
*color8: {{ alt_black }} | |
! red | |
*color1: {{ red }} | |
*color9: {{ alt_red }} | |
! green | |
*color2: {{ green }} | |
*color10: {{ alt_green }} | |
! yellow | |
*color3: {{ yellow }} | |
*color11: {{ alt_yellow }} | |
! blue | |
*color4: {{ blue }} | |
*color12: {{ alt_blue }} | |
! magenta | |
*color5: {{ magenta }} | |
*color13: {{ alt_magenta }} | |
! cyan | |
*color6: {{ cyan }} | |
*color14: {{ alt_cyan }} | |
! white | |
*color7: {{ white }} | |
*color15: {{ alt_white }} | |
! underline when default | |
*colorUL: {% if underline %}{{ underline }}{% else %}{{ white }}{% endif %} | |
xterm*faceName: {{ fontName.title() }} | |
xterm*faceSize: {{ fontSize }} | |
URxvt*font: xft:{{ fontName.title() }}:{{ fontSize }} | |
URxvt.background: {% if transparency %}[{{ transparency }}]{% endif %}{% if background %}{{ background }}{% else %}{{ black }}{% endif %} | |
URxvt.cursorColor: {{ white }} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment