postprefix
10/8/2013 - 8:18 AM

tm2iterm.rb

#!/usr/bin/env ruby
#
# This script is an astonishing feat of top notch
# rockstar craftsmanship. It totally uses artificial
# intelligence to extract colors out of tmTheme and
# build an itermcolors scheme file for iTerm2.
#
# I know this sounds crazy, but it actually knows
# approximately what colors should be used in the
# ANSI list, and tries to find nearest colors from
# the given tmTheme. This level of intelligent guessing
# has never been achieved before in the field of
# converting TextMate themes into iTerm color schemes.
# Never.
#
#
# USAGE
#
# Step 1. Make sure you have plist gem installed
#   gem install plist
#
# Step 2. Make it executable
#   chmod +x tm2iterm.rb
#
# Step 3. Execute it
#   ./tm2iterm.rb /path/to/some.tmTheme /path/to/output.itermcolors
#
# Step 4. Double click the itermcolors file
#   iTerm2 will pick up the scheme and add it to the list

require 'rubygems'
require 'plist'

BRIGHTEN_AMOUNT = 50

class Color
  attr_reader :hex, :rgb, :coefficients, :coef_hash

  def initialize(hex = nil)
    hex ||= '000000'

    if hex =~ /\A#/
      hex.slice!(0)
    end

    @hex = hex[0..5]

    @rgb = @hex.scan(/../).map do |hex_component|
      cap_between_0_and_255(hex_component.to_i(16))
    end

    @coefficients = @rgb.map do |value|
      value / 255.0
    end

    @coef_hash = {
      'Red Component'   => @coefficients[0],
      'Green Component' => @coefficients[1],
      'Blue Component'  => @coefficients[2]
    }
  end

  def brighten(value = BRIGHTEN_AMOUNT)
    hex_string = @rgb.map do |component|
      cap_between_0_and_255(component + value).to_s(16)
    end.join

    self.class.new(hex_string)
  end

  alias to_s hex

  def inspect
    "<Color:##{self}>"
  end

  def sum_distance(color)
    [0, 1, 2].map{ |i| (color.rgb[i] - rgb[i]).abs }.inject(0){ |sum, v| sum += v }
  end

  def find_nearest(colors)
    colors.min do |a, b|
      sum_distance(a) <=> sum_distance(b)
    end
  end

  def ==(color)
    color.rgb == rgb
  end

  private
  def cap_between_0_and_255(num)
    num = [num, 0].max
    num = [num, 255].min
    num
  end
end

# Shortcut
def c(hex)
  Color.new(hex)
end

input, output = ARGV
if input.nil? || output.nil?
  puts 'FAIL. OMG.'
  puts 'Usage: tm2iterm /path/to/file.tmTheme /path/to/file.itermcolors'
  exit(1)
end

tm_theme = Plist::parse_xml(input)

head = tm_theme['settings'].shift['settings']
tail = Hash[tm_theme['settings'].map do |s|
  [ s['name'].downcase, s['settings']['foreground'] ]
end]

background    = c head['background']
bold          = c head['foreground']
cursor        = c head['caret']
cursor_text   = c head['foreground']
foreground    = c head['foreground']
selected_text = c head['foreground']
selection     = c head['selection']

# black   #000000
# red     #ff0000
# green   #00ff00
# yellow  #ffff00
# blue    #0000ff
# magenta #ff00ff
# cyan    #00ffff
# white   #ffffff

ref_color_list = %w[ #000000
                     #ff0000
                     #00ff00
                     #ffff00
                     #0000ff
                     #ff00ff
                     #00ffff
                     #ffffff ].map{ |color| c(color) }

src_color_list = tail.values.uniq.map{ |color| c(color) }

ansi = []

8.times do |i|
  if matching_color = src_color_list.find{|color| color == ref_color_list[i]}
    ansi[i] = matching_color
    src_color_list.delete(matching_color)
  end
end

8.times do |i|
  ansi[i] = ansi[i] || begin
    nearest = ref_color_list[i].find_nearest(src_color_list)
    src_color_list.delete(nearest)
    nearest
  end
end


8.times do |i|
  ansi[i + 8]  = ansi[i].brighten
end

scheme = {
  'Background Color'    => background.coef_hash,
  'Bold Color'          => bold.coef_hash,
  'Cursor Color'        => cursor.coef_hash,
  'Cursor Text Color'   => cursor_text.coef_hash,
  'Foreground Color'    => foreground.coef_hash,
  'Selected Text Color' => selected_text.coef_hash,
  'Selection Color'     => selection.coef_hash,
}

16.times do |i|
  scheme.merge!("Ansi #{i} Color" => ansi[i].coef_hash)
end

File.open(output, 'w') do |file|
  file.write(Plist::Emit.dump(scheme))
end