Skip to content

Instantly share code, notes, and snippets.

@jojonas
Last active March 28, 2024 16:22
Show Gist options
  • Star 19 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save jojonas/fb7a26ccdaa721afa3fe to your computer and use it in GitHub Desktop.
Save jojonas/fb7a26ccdaa721afa3fe to your computer and use it in GitHub Desktop.
Converter for Sublime Text themes to VIM themes
from __future__ import print_function
import sys
import argparse
import os.path
import textwrap
import re
from pprint import pprint
import xml.etree.ElementTree as ET
class TermColors:
LOOKUPTABLE = [
('00', '000000'), ('01', '800000'), ('02', '008000'), ('03', '808000'),
('04', '000080'), ('05', '800080'), ('06', '008080'), ('07', 'c0c0c0'),
('08', '808080'), ('09', 'ff0000'), ('10', '00ff00'), ('11', 'ffff00'),
('12', '0000ff'), ('13', 'ff00ff'), ('14', '00ffff'), ('15', 'ffffff'),
('16', '000000'), ('17', '00005f'), ('18', '000087'), ('19', '0000af'),
('20', '0000d7'), ('21', '0000ff'), ('22', '005f00'), ('23', '005f5f'),
('24', '005f87'), ('25', '005faf'), ('26', '005fd7'), ('27', '005fff'),
('28', '008700'), ('29', '00875f'), ('30', '008787'), ('31', '0087af'),
('32', '0087d7'), ('33', '0087ff'), ('34', '00af00'), ('35', '00af5f'),
('36', '00af87'), ('37', '00afaf'), ('38', '00afd7'), ('39', '00afff'),
('40', '00d700'), ('41', '00d75f'), ('42', '00d787'), ('43', '00d7af'),
('44', '00d7d7'), ('45', '00d7ff'), ('46', '00ff00'), ('47', '00ff5f'),
('48', '00ff87'), ('49', '00ffaf'), ('50', '00ffd7'), ('51', '00ffff'),
('52', '5f0000'), ('53', '5f005f'), ('54', '5f0087'), ('55', '5f00af'),
('56', '5f00d7'), ('57', '5f00ff'), ('58', '5f5f00'), ('59', '5f5f5f'),
('60', '5f5f87'), ('61', '5f5faf'), ('62', '5f5fd7'), ('63', '5f5fff'),
('64', '5f8700'), ('65', '5f875f'), ('66', '5f8787'), ('67', '5f87af'),
('68', '5f87d7'), ('69', '5f87ff'), ('70', '5faf00'), ('71', '5faf5f'),
('72', '5faf87'), ('73', '5fafaf'), ('74', '5fafd7'), ('75', '5fafff'),
('76', '5fd700'), ('77', '5fd75f'), ('78', '5fd787'), ('79', '5fd7af'),
('80', '5fd7d7'), ('81', '5fd7ff'), ('82', '5fff00'), ('83', '5fff5f'),
('84', '5fff87'), ('85', '5fffaf'), ('86', '5fffd7'), ('87', '5fffff'),
('88', '870000'), ('89', '87005f'), ('90', '870087'), ('91', '8700af'),
('92', '8700d7'), ('93', '8700ff'), ('94', '875f00'), ('95', '875f5f'),
('96', '875f87'), ('97', '875faf'), ('98', '875fd7'), ('99', '875fff'),
('100', '878700'), ('101', '87875f'), ('102', '878787'), ('103', '8787af'),
('104', '8787d7'), ('105', '8787ff'), ('106', '87af00'), ('107', '87af5f'),
('108', '87af87'), ('109', '87afaf'), ('110', '87afd7'), ('111', '87afff'),
('112', '87d700'), ('113', '87d75f'), ('114', '87d787'), ('115', '87d7af'),
('116', '87d7d7'), ('117', '87d7ff'), ('118', '87ff00'), ('119', '87ff5f'),
('120', '87ff87'), ('121', '87ffaf'), ('122', '87ffd7'), ('123', '87ffff'),
('124', 'af0000'), ('125', 'af005f'), ('126', 'af0087'), ('127', 'af00af'),
('128', 'af00d7'), ('129', 'af00ff'), ('130', 'af5f00'), ('131', 'af5f5f'),
('132', 'af5f87'), ('133', 'af5faf'), ('134', 'af5fd7'), ('135', 'af5fff'),
('136', 'af8700'), ('137', 'af875f'), ('138', 'af8787'), ('139', 'af87af'),
('140', 'af87d7'), ('141', 'af87ff'), ('142', 'afaf00'), ('143', 'afaf5f'),
('144', 'afaf87'), ('145', 'afafaf'), ('146', 'afafd7'), ('147', 'afafff'),
('148', 'afd700'), ('149', 'afd75f'), ('150', 'afd787'), ('151', 'afd7af'),
('152', 'afd7d7'), ('153', 'afd7ff'), ('154', 'afff00'), ('155', 'afff5f'),
('156', 'afff87'), ('157', 'afffaf'), ('158', 'afffd7'), ('159', 'afffff'),
('160', 'd70000'), ('161', 'd7005f'), ('162', 'd70087'), ('163', 'd700af'),
('164', 'd700d7'), ('165', 'd700ff'), ('166', 'd75f00'), ('167', 'd75f5f'),
('168', 'd75f87'), ('169', 'd75faf'), ('170', 'd75fd7'), ('171', 'd75fff'),
('172', 'd78700'), ('173', 'd7875f'), ('174', 'd78787'), ('175', 'd787af'),
('176', 'd787d7'), ('177', 'd787ff'), ('178', 'd7af00'), ('179', 'd7af5f'),
('180', 'd7af87'), ('181', 'd7afaf'), ('182', 'd7afd7'), ('183', 'd7afff'),
('184', 'd7d700'), ('185', 'd7d75f'), ('186', 'd7d787'), ('187', 'd7d7af'),
('188', 'd7d7d7'), ('189', 'd7d7ff'), ('190', 'd7ff00'), ('191', 'd7ff5f'),
('192', 'd7ff87'), ('193', 'd7ffaf'), ('194', 'd7ffd7'), ('195', 'd7ffff'),
('196', 'ff0000'), ('197', 'ff005f'), ('198', 'ff0087'), ('199', 'ff00af'),
('200', 'ff00d7'), ('201', 'ff00ff'), ('202', 'ff5f00'), ('203', 'ff5f5f'),
('204', 'ff5f87'), ('205', 'ff5faf'), ('206', 'ff5fd7'), ('207', 'ff5fff'),
('208', 'ff8700'), ('209', 'ff875f'), ('210', 'ff8787'), ('211', 'ff87af'),
('212', 'ff87d7'), ('213', 'ff87ff'), ('214', 'ffaf00'), ('215', 'ffaf5f'),
('216', 'ffaf87'), ('217', 'ffafaf'), ('218', 'ffafd7'), ('219', 'ffafff'),
('220', 'ffd700'), ('221', 'ffd75f'), ('222', 'ffd787'), ('223', 'ffd7af'),
('224', 'ffd7d7'), ('225', 'ffd7ff'), ('226', 'ffff00'), ('227', 'ffff5f'),
('228', 'ffff87'), ('229', 'ffffaf'), ('230', 'ffffd7'), ('231', 'ffffff'),
('232', '080808'), ('233', '121212'), ('234', '1c1c1c'), ('235', '262626'),
('236', '303030'), ('237', '3a3a3a'), ('238', '444444'), ('239', '4e4e4e'),
('240', '585858'), ('241', '626262'), ('242', '6c6c6c'), ('243', '767676'),
('244', '808080'), ('245', '8a8a8a'), ('246', '949494'), ('247', '9e9e9e'),
('248', 'a8a8a8'), ('249', 'b2b2b2'), ('250', 'bcbcbc'), ('251', 'c6c6c6'),
('252', 'd0d0d0'), ('253', 'dadada'), ('254', 'e4e4e4'), ('255', 'eeeeee'),
]
@classmethod
def hex2rgb(cls, hex):
hex = hex.strip().lstrip("#")
r = hex[0:2]
g = hex[2:4]
b = hex[4:6]
return tuple( int(h, 16) for h in (r,g,b) )
@classmethod
def hex2lightness(cls, hex):
r,g,b = cls.hex2rgb(hex)
r /= 255.0
g /= 255.0
b /= 255.0
return (0.2126*r + 0.7152*g + 0.0722*b)
@classmethod
def _color_distance(cls, rgb1, rgb2):
return abs(rgb1[0]-rgb2[0]) + abs(rgb1[1]-rgb2[1]) + abs(rgb1[2]-rgb2[2])
@classmethod
def _closest(cls, hex):
rgb = cls.hex2rgb(hex)
distance = lambda x: cls._color_distance(rgb, cls.hex2rgb(x[1]))
return min(cls.LOOKUPTABLE, key=distance)
@classmethod
def hex2hex(cls, hex):
if not hex or len(hex) != 7:
return None
return "#" + cls._closest(hex)[1].upper()
@classmethod
def hex2short(cls, hex):
if not hex or len(hex) != 7:
return None
return cls._closest(hex)[0]
class VimHirule:
@classmethod
def from_tmrule(cls, name, dict):
return cls(
name=name,
fg=dict.get("foreground", None),
bg=dict.get("background", None),
style=dict.get("fontStyle", None),
)
def __init__(self, name, fg=None, bg=None, sp=None, style=None):
self.name = name
self.guifg = fg
self.guibg = bg
self.guisp = sp
self.gui = style
@property
def ctermfg(self):
return TermColors.hex2short(self.guifg)
@property
def ctermbg(self):
return TermColors.hex2short(self.guibg)
@property
def cterm(self):
return self.gui
def line(self, full=True):
return "hi {name:<15s} guifg={guifg} guibg={guibg} guisp={guisp} gui={gui} " \
"ctermfg={ctermfg} ctermbg={ctermbg} cterm={cterm}".format(
name=self.name,
guifg=self.guifg,
guibg=self.guibg,
guisp=self.guisp,
gui=self.gui,
ctermfg=self.ctermfg,
ctermbg=self.ctermbg,
cterm=self.cterm,
)
class TmThemeReader:
def __init__(self, filename):
tree = ET.parse(filename)
root = tree.getroot()
self.properties = self._parse_propertylist(root)
self.hirules = self._compile_highlighting_rules()
self.color_settings = self.properties["settings"][0]["settings"]
self.gutter_settings = self.properties.get("gutterSettings", {})
def match(self, scope):
match = None
for setting in self.hirules:
if setting.startswith(scope) and (match is None or len(match) > len(setting)):
match = setting
if match:
return self.hirules[match]
@classmethod
def _parse_propertylist(cls, root):
if root.tag == 'string':
return root.text
elif root.tag == 'plist':
return cls._parse_propertylist(root.find('dict'))
elif root.tag == 'array':
list = []
for element in root.findall('*'):
value = cls._parse_propertylist(element)
list.append(value)
return list
elif root.tag == 'dict':
dict = {}
iterator = iter(root.findall('*'))
for element in iterator:
if element.tag == 'key':
key = element.text
value = cls._parse_propertylist(next(iterator))
dict[key] = value
else:
raise ValueError("Unexpected tag in dictionary: '%s'.", element.tag)
return dict
def _compile_highlighting_rules(self):
dict = {}
for setting in self.properties['settings']:
if 'name' in setting and 'settings' in setting:
scope = setting['scope']
settings = setting['settings']
for name in scope.split(","):
name = name.strip()
dict[name] = settings
return dict
def convert(filename, outfile=sys.stdout):
reader = TmThemeReader(filename)
vim_names = {
"Number": "constant.numeric",
"Character": "constant.character",
"String": "string",
"Constant": "constant",
"Identifier": "variable",
"Keyword": "keyword",
"Comment": "comment",
"Operator": "keyword.operator",
"Statement": "variable.parameter.function",
"Type": ("entity.name.class", "meta.class", "support.class"),
"StorageClass": "storage",
"Function": ("entity.name.function", "support.function"),
}
rules = []
for name, candidates in vim_names.items():
if not isinstance(candidates, tuple):
candidates = (candidates, )
for candidate in candidates:
match = reader.match(candidate)
if match:
rules.append(VimHirule.from_tmrule(name, match))
break
rules.append(VimHirule.from_tmrule("Normal", reader.color_settings))
rules.append(VimHirule.from_tmrule("LineNr", reader.gutter_settings))
if "lineHighlight" in reader.color_settings:
rules.append(VimHirule("CursorLine", bg=reader.color_settings["lineHighlight"]))
if "selection" in reader.color_settings:
rules.append(VimHirule("Visual", bg=reader.color_settings["selection"]))
if "findHighlight" in reader.color_settings:
rules.append(VimHirule("Search", bg=reader.color_settings["findHighlight"], fg=reader.color_settings["findHighlightForeground"]))
if "caret" in reader.color_settings:
rules.append(VimHirule("Cursor", bg=reader.color_settings["caret"]))
hilines = []
for rule in sorted(rules, key=lambda r: r.name):
hilines.append(rule.line())
background = "dark"
if "background" in reader.color_settings:
lightness = TermColors.hex2lightness(reader.color_settings["background"])
if lightness > 0.5:
background = "light"
content="""
" VIM color file
"
" Note: Based on the {theme_name} theme for Sublime Text
{author_line}
hi clear
set background={background}
if version > 580
if exists("syntax_on")
syntax reset
endif
endif
set t_Co=256
let g:colors_name="{theme_name}"
{hilines}
hi link Conditional Keyword
hi link Repeat Keyword
hi link cType Keyword
"""
content = textwrap.dedent(content)[1:].format(
background=background,
author_line=("\" by " + reader.properties["author"]) if "author" in reader.properties else "",
theme_name=reader.properties["name"],
hilines="\n".join(hilines),
)
print(content, file=outfile)
if __name__=="__main__":
parser = argparse.ArgumentParser(description="Converter for Sublime Text themes to VIM themes.")
parser.add_argument("tmtheme", type=str, nargs="+", help="ST theme file to convert.")
parser.add_argument("-o", "--out", type=str, help="Write to this file (default: stdout).")
parser.add_argument("--install", dest="install", action="store_true", help="Write directly to .vim/colors folder.")
args = parser.parse_args()
for theme in args.tmtheme:
name, ext = os.path.splitext(theme)
if ext.lower() != ".tmtheme":
raise ValueError("Can only convert .tmTheme files.")
filename = None
if args.out:
filename = args.out
elif args.install:
if os.name == "nt":
vimcolors = os.path.join("~", "vimfiles", "colors")
else:
vimcolors = os.path.join("~", ".vim", "colors")
vimcolors = os.path.expanduser(vimcolors)
filename = os.path.join(vimcolors, name + ".vim")
if filename:
print("Writing to '%s'." % filename)
with open(filename, 'w') as file:
convert(theme, outfile=file)
else:
convert(theme)
@RobinBoers
Copy link

Thank you so much for making this! I really wanted a specific color scheme, but I couldnt find it anywhere. This saved my day!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment