Last active
April 22, 2016 02:53
-
-
Save silverhammermba/14833f5f8fe7ea2524c6ed93d50ab74b to your computer and use it in GitHub Desktop.
Generate awesome theme from background image
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 ruby | |
# pick a random background and generate an awesome theme based on it | |
require 'color' | |
require 'color/palette/monocontrast' | |
require 'erb' | |
require 'fileutils' | |
require 'imlib2' | |
require 'optparse' | |
require 'shellwords' | |
class ThemeError < StandardError; end | |
# get the theme path for an image | |
def get_theme_path path | |
File.join File.dirname(path), ".#{File.basename path}.theme" | |
end | |
# set the theme from an image path | |
def set_theme path | |
Dir.chdir File.dirname(__FILE__) | |
FileUtils.rm_f 'theme' | |
FileUtils.ln_s get_theme_path(path), 'theme' | |
end | |
# check if an image path has a theme | |
def has_theme? path | |
File.directory? get_theme_path(path) | |
end | |
# make an awesome theme from a background | |
def make_theme bg_path | |
# TODO use a hidden file name | |
theme_path = get_theme_path bg_path | |
file = File.basename bg_path | |
raise ThemeError, "theme already exists for `#{file}'" if File.exists? theme_path | |
# must be able to load image | |
begin | |
image = Imlib2::Image.load bg_path | |
rescue Imlib2::Error, Imlib2::FileError | |
raise ThemeError, "cannot load image `#{file}': #$!" | |
end | |
# get the top row of pixels | |
pixels = (0...image.width).map do |x| | |
color = image.pixel x, 0 | |
Color::RGB.new(color.r, color.g, color.b) | |
end | |
# pick an (arbitrary) representative pixel | |
# TODO would this benefit from some kind of average? | |
base = pixels[pixels.length / 2] | |
lab = base.to_lab | |
# top row of pixels must be essentially uniform | |
raise ThemeError, "`#{file}' has non-uniform colors" if pixels.any? { |p| base.delta_e94(lab, p.to_lab) > 1 } | |
# generate contrasting color | |
con = base.to_hsl | |
con.hue += 180 | |
# should be at least somewhat saturated | |
con.saturation = [50, con.saturation].max | |
# shouldn't be too light or dark (in case base is very light or dark) | |
con.luminosity = [25, con.luminosity, 75].sort[1] | |
con = con.to_rgb | |
# generate monochrome contrasting color palettes | |
palette = Color::Palette::MonoContrast.new(base) | |
con_palette = Color::Palette::MonoContrast.new(con) | |
base_i = 0 | |
# try to find a muted color | |
mute_i = 5 | |
while mute_i > 0 | |
mi = mute_i - 1 | |
# if this doesn't change the color, or it's still a distinct color from the base, step down | |
if palette.background[mute_i] == palette.background[mi] || (palette.background[mi] != palette.background[base_i] && mi >= 3) | |
mute_i = mi | |
else | |
break | |
end | |
end | |
# mute_i might equal base_i | |
# try to find a highlighted color | |
high_i = -5 | |
while high_i < 0 | |
hi = high_i + 1 | |
if palette.background[high_i] == palette.background[hi] || (palette.background[hi] != palette.background[base_i] && hi <= -3) | |
high_i = hi | |
else | |
break | |
end | |
end | |
# high_i might equal base_i | |
# but it should be impossible for base_i == high_i == mute_i | |
# if base is really light, make muted color slightly less dark than the highlighted one | |
if mute_i == base_i | |
mute_i = (base_i + high_i) / 2 | |
end | |
# if base is really dark, make the muted color the highlighted one and put muted in between | |
if high_i == base_i | |
high_i = mute_i | |
mute_i = (base_i + high_i) / 2 | |
end | |
# variables for template | |
# full path to background | |
wallpaper = bg_path | |
# base colors | |
base_bg = palette.background[base_i].html | |
base_fg = palette.foreground[base_i].html | |
# muted colors | |
mute_bg = palette.background[mute_i].html | |
mute_fg = palette.foreground[mute_i].html | |
# highlighted colors | |
high_bg = palette.background[high_i].html | |
high_fg = palette.foreground[high_i].html | |
# standout colors | |
stand_bg = con_palette.background[base_i].html | |
stand_fg = con_palette.foreground[base_i].html | |
begin | |
# create the theme | |
Dir.mkdir theme_path | |
Dir.chdir File.join(File.dirname(__FILE__), 'theme.template') | |
# copy images | |
%w{layouts menu taglist}.each do |dir| | |
FileUtils.cp_r dir, theme_path | |
end | |
# recolor images | |
`mogrify -fill "#{base_fg}" -opaque white #{File.join(theme_path, 'layouts', '*').shellescape}` | |
`mogrify -fill "#{stand_bg}" -opaque white #{File.join(theme_path, 'menu', '*').shellescape}` | |
`mogrify -fill "#{high_fg}" -opaque white #{File.join(theme_path, 'taglist', 'sel.png').shellescape}` | |
`mogrify -fill "#{base_fg}" -opaque white #{File.join(theme_path, 'taglist', 'unsel.png').shellescape}` | |
File.open(File.join(theme_path, 'theme.lua'), 'w') do |f| | |
f.puts ERB.new(File.read('theme.lua.erb')).result(binding) | |
end | |
rescue | |
FileUtils.rm_rf theme_path | |
raise | |
end | |
end | |
# parse options | |
$pick = true | |
opt = OptionParser.new do |opts| | |
opts.banner = "USAGE: #$0 [-g|--generate] DIR" | |
opts.on '-g', '--generate', 'just generate themes' do | |
$pick = false | |
end | |
end | |
opt.parse! | |
if ARGV.size != 1 | |
warn opt | |
exit 1 | |
end | |
# get full bg path | |
bg_dir = File.expand_path(ARGV[0]) | |
# if path is a file, assume it is an image. make and set the theme | |
if File.file? bg_dir | |
bg = bg_dir | |
make_theme bg unless has_theme? bg | |
set_theme bg | |
exit | |
end | |
# otherwise assume it is a background dir | |
# find all files in background dir | |
backgrounds = Dir.entries(bg_dir).map { |e| File.join(bg_dir, e) }.select { |p| File.file? p } | |
# figure out which have themes | |
themed, unthemed = backgrounds.partition { |p| has_theme? p } | |
if $pick | |
# need a pre-existing theme | |
if themed.empty? | |
warn "no themed backgrounds exist!" | |
exit 1 | |
end | |
# pick one randomly | |
srand | |
set_theme themed.sample | |
# detach a child process and exit | |
pid = Process.fork | |
if pid | |
Process.detach pid | |
exit | |
end | |
end | |
# build themes for the unthemed backgrounds | |
unthemed.each do |bg| | |
begin | |
make_theme bg | |
rescue ThemeError | |
warn $! | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment