Last active
March 28, 2024 16:22
-
-
Save jojonas/fb7a26ccdaa721afa3fe to your computer and use it in GitHub Desktop.
Converter for Sublime Text themes to VIM themes
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
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) | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Thank you so much for making this! I really wanted a specific color scheme, but I couldnt find it anywhere. This saved my day!