Skip to content

Instantly share code, notes, and snippets.

@silverhammermba
Last active April 22, 2016 02:53
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save silverhammermba/14833f5f8fe7ea2524c6ed93d50ab74b to your computer and use it in GitHub Desktop.
Save silverhammermba/14833f5f8fe7ea2524c6ed93d50ab74b to your computer and use it in GitHub Desktop.
Generate awesome theme from background image
#!/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