I remember spending hours when I was younger cycling through the various awesome color themes on my 386, in the glory days of windows 3.1. Remember hotdog stand?

Hotdog Stand

Well, I haven't changed much. I still enjoy making tweaks to the colors and appearance of my desktop. In this post I'll talk about a script I wrote that makes it easy for me to modify all the various colors and configuration files which control the appearance of my desktop.

Below is a screenshot of my current desktop setup. I've made the windows transparent so you can see the wallpaper. The left-hand side shows the output of two scripts which show off my 16-color terminal colorscheme. The right-hand side shows where I spend most of my time, the text editor. At the bottom is a little ribbon that shows my active "virtual desktops", as well as some status info, a clock and my tray icons (sound volume, wifi, and dropbox).

I really like to freshen up my colorscheme every month or two, but as I'm particular about everything matching, it takes some time to do it right and I'm not always sure how it will turn out. I also like to save my themes so I can switch back to them, but since there are so many files that contribute to the appearance, it often was more trouble than it was worth to try and "save" them.

So I did what any programmer would do, I decided to write a script to make it really easy for me to change up my colorschemes.

Here are some links to some themes I've generated with my script. They have cheesy names, but it helps me remember which is which.

In case you're curious, I'm using i3, a "tiling" window manager. Unlike Windows or OS X, my windows are all placed flat and "tiled", and instead of stacking them I have multiple virtual desktops. Check out the screencast:

"themer" in action

The script itself is pretty easy to use, and hopefully very hackable if you're interested in tailoring it to your desktop. The screenshot below shows how I typically use themer.

  • themer list to list out all the themes I've currently got
  • themer current displays the name of my current theme and what the colors look like
  • themer generate bloom ~/pictures/Wallpapers/bloom.jpg generates a new theme based on a nice wallpaper I found (wallpaper displayed in bottom-left).
  • themer list again to see that "bloom" was added
  • Then lastly I open up a browser and view the auto-generated "index.html" file which displays the new colorscheme as an HTML page.

The source code

The script itself is over 500 lines of (hacked together) code, too long to just slap into this blog post, so I've put it up on github as a gist:

https://gist.github.com/coleifer/33484bff21c34644dae1

I've also included some templates to get you started if you'd like to try it out for yourself. These belong in the "default" template folder, ~/.themer/templates/i3/ (though of course you can change this to suit you). The templates and config are in this gist:

https://gist.github.com/coleifer/9fbab2d19a337512ab2a

Additionally, the script relies on a helper I wrote called wallfix which automatically resizes and crops an image to make it suitable for use as a wallpaper. The cropped wallpaper is stored in $HOME/.wallpaper.[png|jpg].

A color for every occasion

Sometimes I would see a really cool colortheme on the unixporn subreddit (don't worry, its safe for work), so my initial plan was to be able to take the 16 color Xresources file and generate everything else from that.

A while ago I found a really cool site that will automatically generate color schemes for popular text editors. Sweyla's site let's you cycle through thousands of programmatically generated color themes by clicking a button -- it's pretty slick and some of the themes look great so I knew I wanted my script to also be able to use themes from Sweyla's site.

And finally, back when I wrote about using k-means to detect the dominant colors in images, a friendly redditor shared a script he wrote that will normalize the colors extracted from wallpaper images so he could generate Xresources files directly from his wallpaper. I borrowed his code for "normalizing" colors so my script could generate colorschemes directly from a wallpaper I liked.

So the script will accept three possible inputs:

  • Xresources color definitions
  • A set of colors from Sweyla's website
  • An image file (which will also be the wallpaper)

The script is then responsible for updating my various config files. Here is the list of things I change whenever updating my colors:

  • Xresources, the 16 colors I use on my terminal
  • Window borders (i3 uses a simple text file to configure these)
  • Status bar and program launcher (i3status and dmenu_run)
  • Wallpaper
  • Icons (I use ACYL, a customizable set of svg icons).
  • Text editor

Setting things up

A long time ago a former coworker of mine wrote some utility scripts that used a combination of python, YAML and Jinja2 to template out his various system config files. I think this is brilliant and decided to go with the same combination for my script. It reads a YAML file that defines variables controlling things like the font, window borders, etc. The config file also defines the locations of the templates for rendering my .Xresources, .i3/config, etc.

Here is what my main config currently looks like:

variables:
  primary: magenta
  secondary: green
  tertiary: red
  special: yellow
  transparency: 85
  fontName: terminus
  fontSize: 12
  font: "-*-terminus-*-*-*-*-12-*-*-*-*-*-*-*"
  window_style: 1pixel
files:
  colors.tpl: colors
  i3.tpl: i3.conf
  i3status.tpl: i3status.conf
  xresources.tpl: Xresources
  index.tpl: index.html

The variables section defines things that I may want to change later, such as the font I use, or which colors I treat as primary or secondary. The files section contains a mapping of template file to destination file. These are just jinja templates -- here is a snippet from the xresources.tpl:

*faceName: {{ fontName.title() }}
*faceSize: {{ fontSize }}
*font: xft:{{ fontName.title() }}:{{ fontSize }}
*.cursorColor: {% if foreground %}{{ foreground }}{% else %}{{ white }}{% endif %}

As you can see, this template uses some variables defined in the variables section of the YAML config file. It also uses one that isn't defined in the config: white. Because the script will programmatically determine which colors to use, it will be responsible for adding them to the template context.

Here is the code for reading the configuration files. It supports config-file inheritance, although I haven't yet found myself using this:

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)

By now I imagine you see how this all works -- so how does the script figure out which colors to use?

Getting the colors

As I mentioned earlier, the colors are read from one of three sources so I just needed a way to determine what to do given the input. It works like this:

  • If the input is a bunch of numbers, consider it the "seed" for one of Sweyla's themes, and use urllib2 to go fetch the colors and convert them into something usable.
  • If the input is a file and ends with ".jpg" or ".png" treat it as a wallpaper and use the k-means algorithm to extract 16 colors from the image file (and do a bit of normalization as well).
  • Otherwise, treat the file as an Xresources-type file and simply parse it for the color definitions.

Each of these is implemented as a different ColorParser. For example, the "Sweyla" color parser fetches the pygments color theme and then maps the values to colors used by the templates:

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

Another trick is to update the colors of the icons I use on my desktop. There's an icon theme called "any color you like" that consists of a bunch of svg icon files and a python script which lets you modify the icon colors (so your folders/files could all be one color and your programs another). These icons are really just fancy text files, so the script will introspect the colors of the icons you're currently using and do a big "search and replace" to swap the old colors with the new ones.

Putting it all together

As I hope I've shown, generating a new theme consists of little more than determining which colors to use and rendering a set of templated config files. The other important task the script does is make it easy to "activate" a theme once generated.

Here are all of the things that the script does when you activate a new theme:

  • Put rendered templates into their own folder so I can have a collection of themes ready when I want to change it up.
  • Symlink the active set of templates to a well-known location, so I can in turn symlink the "active" templates to where my configuration files live. For example ~/.themer/zenburn/ is symlinked to ~/.themer/current/ and then ~/.themer/current/Xresouces is symlinked to ~/.Xresources.
  • If a wallpaper wasn't specified, generate a geometric pattern using the colors from the theme.
  • Set the wallpaper (using the wallfix script).
  • Change the icon colors.
  • Generate an HTML file "demoing" the colors so I can remember what they look like.
  • Other junk like calling xrdb -merge and restarting my window manager.

I've had a ton of fun hacking this together and imagine I will continue to make tweaks over time. While the script and config files are fairly tied to my own setup, I hope that if you're interested, you are able to modify and expand on them.

Thanks for reading! Please feel free to leave any comments, questions or suggestions below.

Links

Comments (5)

  • Stephen Tanner | August 2013, at 15:34

    This is most good! I use i3 myself and am curious what compositor you are using for transparency?


  • Charlie | August 2013, at 16:21

    I used xcompmgr for transparency in those screenshots, although I don't usually run a compositor.


  • Wann | August 2013, at 07:43

    Hi, great work, but I'm keep getting this error when trying to run this script on Manjaro Linux with python 3.3:

    File "themer.py", line 488
        print "\n".join(sorted(themes))
    

  • Charlie | August 2013, at 09:55

    The script was written with python 2 in mind, as that is what I still use. You can fix this by calling the print function: print("\n".join(sorted(themes))). There are probably other places that will need to be updated as well, unfortunately.


  • Wann | August 2013, at 13:13

    Thanks! Works like a charm with python2 after installation of python2-yaml and python2-jinja modules.


Commenting has been closed, but please feel free to contact me