Skip to content

Instantly share code, notes, and snippets.

@FelisDiligens
Last active December 7, 2023 15:21
Show Gist options
  • Save FelisDiligens/a182bf3e178887b1b4e7cf620bbec613 to your computer and use it in GitHub Desktop.
Save FelisDiligens/a182bf3e178887b1b4e7cf620bbec613 to your computer and use it in GitHub Desktop.
Converts theme files between different formats (Windows Terminal, mintty, iTerm)
#!/usr/bin/env python3
import json
import configparser
import xml.etree.ElementTree as ET
from pathlib import Path
import sys
import argparse
def clamp(val, min_val, max_val):
return max(min_val, min(val, max_val))
class Color:
"""Holds a RGB color. Can convert to and from various notations."""
type_rgb_int = "rgb_int"
type_rgb_str = "rgb_str"
type_rgb_real = "rgb_real"
type_hex = "hex"
def __init__(self, color, format="rgb_int"):
"""
Create a Color object.
Arguments:
color -- either a tuple or a str, depending on type.
format -- one of the Color.type_* fields
"""
if format == Color.type_rgb_int:
if type(color) is not tuple or len(color) != 3:
raise ValueError(
f"Invalid 'color' argument. Expected tuple with 3 elements, got '{color}'"
)
self.r = color[0]
self.g = color[1]
self.b = color[2]
elif format == Color.type_rgb_str:
if type(color) is not str:
raise ValueError(
f"Invalid 'color' argument. Expected string, got '{color}'"
)
rgb = tuple((int(s) for s in color.split(",")))
if len(rgb) != 3:
raise ValueError(
f"Invalid 'color' argument. Expected string with three commas, got '{color}'"
)
self.r = int(rgb[0])
self.g = int(rgb[1])
self.b = int(rgb[2])
elif format == Color.type_rgb_real:
if type(color) is not tuple or len(color) != 3:
raise ValueError(
f"Invalid 'color' argument. Expected tuple with 3 elements, got '{color}'"
)
self.r = clamp(int(color[0] * 255), 0, 255)
self.g = clamp(int(color[1] * 255), 0, 255)
self.b = clamp(int(color[2] * 255), 0, 255)
elif format == Color.type_hex:
if type(color) is not str:
raise ValueError(
f"Invalid 'color' argument. Expected string, got '{color}'"
)
hex = color.lstrip("#")
rgb = tuple(int(hex[i : i + 2], 16) for i in (0, 2, 4))
self.r = rgb[0]
self.g = rgb[1]
self.b = rgb[2]
else:
raise ValueError(f"Unknown type '{format}'")
def get_rgb_real(self) -> tuple[float, float, float]:
return (self.r / 255.0, self.g / 255.0, self.b / 255.0)
def get_rgb_int(self) -> tuple[int, int, int]:
return (self.r, self.g, self.b)
def get_rgb_str(self):
return "%i,%i,%i" % (self.r, self.g, self.b)
def get_hex(self):
return ("#%02x%02x%02x" % (self.r, self.g, self.b)).upper()
class Theme:
def __init__(self):
self.name = "Unnamed"
self.background_color = Color("#FFFFFF", format=Color.type_hex)
self.foreground_color = Color("#FFFFFF", format=Color.type_hex)
self.cursor_color = Color("#FFFFFF", format=Color.type_hex)
self.selection_background = Color("#FFFFFF", format=Color.type_hex)
self.black = Color("#FFFFFF", format=Color.type_hex)
self.bright_black = Color("#FFFFFF", format=Color.type_hex)
self.red = Color("#FFFFFF", format=Color.type_hex)
self.bright_red = Color("#FFFFFF", format=Color.type_hex)
self.green = Color("#FFFFFF", format=Color.type_hex)
self.bright_green = Color("#FFFFFF", format=Color.type_hex)
self.yellow = Color("#FFFFFF", format=Color.type_hex)
self.bright_yellow = Color("#FFFFFF", format=Color.type_hex)
self.blue = Color("#FFFFFF", format=Color.type_hex)
self.bright_blue = Color("#FFFFFF", format=Color.type_hex)
self.purple = Color("#FFFFFF", format=Color.type_hex)
self.bright_purple = Color("#FFFFFF", format=Color.type_hex)
self.cyan = Color("#FFFFFF", format=Color.type_hex)
self.bright_cyan = Color("#FFFFFF", format=Color.type_hex)
self.white = Color("#FFFFFF", format=Color.type_hex)
self.bright_white = Color("#FFFFFF", format=Color.type_hex)
def from_wt(self, theme):
json_parsed = json.loads(theme)
self.name = json_parsed["name"]
self.background_color = Color(json_parsed["background"], format=Color.type_hex)
self.foreground_color = Color(json_parsed["foreground"], format=Color.type_hex)
self.cursor_color = Color(json_parsed["cursorColor"], format=Color.type_hex)
self.selection_background = Color(
json_parsed["selectionBackground"], format=Color.type_hex
)
self.black = Color(json_parsed["black"], format=Color.type_hex)
self.bright_black = Color(json_parsed["brightBlack"], format=Color.type_hex)
self.red = Color(json_parsed["red"], format=Color.type_hex)
self.bright_red = Color(json_parsed["brightRed"], format=Color.type_hex)
self.green = Color(json_parsed["green"], format=Color.type_hex)
self.bright_green = Color(json_parsed["brightGreen"], format=Color.type_hex)
self.yellow = Color(json_parsed["yellow"], format=Color.type_hex)
self.bright_yellow = Color(json_parsed["brightYellow"], format=Color.type_hex)
self.blue = Color(json_parsed["blue"], format=Color.type_hex)
self.bright_blue = Color(json_parsed["brightBlue"], format=Color.type_hex)
self.purple = Color(json_parsed["purple"], format=Color.type_hex)
self.bright_purple = Color(json_parsed["brightPurple"], format=Color.type_hex)
self.cyan = Color(json_parsed["cyan"], format=Color.type_hex)
self.bright_cyan = Color(json_parsed["brightCyan"], format=Color.type_hex)
self.white = Color(json_parsed["white"], format=Color.type_hex)
self.bright_white = Color(json_parsed["brightWhite"], format=Color.type_hex)
return self
def from_mintty(self, theme):
section = "SECTION"
parser = configparser.ConfigParser()
parser.read_string(f"[{section}]\n" + theme)
for key in parser[section]:
color = Color(parser[section][key], format=Color.type_rgb_str)
if key.lower() == "backgroundcolour":
self.background_color = color
elif key.lower() == "foregroundcolour":
self.foreground_color = color
elif key.lower() == "cursorcolour":
self.cursor_color = color
elif key.lower() == "black":
self.black = color
elif key.lower() == "boldblack":
self.bright_black = color
elif key.lower() == "red":
self.red = color
elif key.lower() == "boldred":
self.bright_red = color
elif key.lower() == "green":
self.green = color
elif key.lower() == "boldgreen":
self.bright_green = color
elif key.lower() == "yellow":
self.yellow = color
elif key.lower() == "boldyellow":
self.bright_yellow = color
elif key.lower() == "blue":
self.blue = color
elif key.lower() == "boldblue":
self.bright_blue = color
elif key.lower() == "magenta":
self.purple = color
elif key.lower() == "boldmagenta":
self.bright_purple = color
elif key.lower() == "cyan":
self.cyan = color
elif key.lower() == "boldcyan":
self.bright_cyan = color
elif key.lower() == "white":
self.white = color
elif key.lower() == "boldwhite":
self.bright_white = color
return self
def from_iterm(self, theme):
# Keys
# ----
# Ansi 0 Color = black
# Ansi 1 Color = red
# Ansi 2 Color = green
# Ansi 3 Color = yellow
# Ansi 4 Color = blue
# Ansi 5 Color = purple
# Ansi 6 Color = cyan
# Ansi 7 Color = white
# Ansi 8 Color = bright_black
# Ansi 9 Color = bright_red
# Ansi 10 Color = bright_green
# Ansi 11 Color = bright_yellow
# Ansi 12 Color = bright_blue
# Ansi 13 Color = bright_purple
# Ansi 14 Color = bright_cyan
# Ansi 15 Color = bright_white
# Background Color = background_color
# Badge Color = ?
# Bold Color = ?
# Cursor Color = cursor_color
# Cursor Guide Color = ?
# Cursor Text Color = ?
# Foreground Color = foreground_color
# Link Color = ?
# Selected Text Color = ?
# Selection Color = selection_background
root = ET.fromstring(theme)
if not root.tag == "plist" or root.attrib["version"] != "1.0":
raise ValueError(
'ITerm XML version unknown, expected <plist version="1.0">'
)
if not root[0].tag == "dict":
raise ValueError("<plist> does not contain <dict>")
key = None
for child in root[0]:
if child.tag == "key":
key = child.text
elif child.tag == "dict":
rgb_dict = {"red": 0.0, "green": 0.0, "blue": 0.0}
rgb_component = None
for rgb in child:
if rgb.tag == "key":
if rgb.text == "Blue Component":
rgb_component = "blue"
elif rgb.text == "Green Component":
rgb_component = "green"
elif rgb.text == "Red Component":
rgb_component = "red"
else:
rgb_component = None
elif rgb.tag == "real" and rgb_component:
rgb_dict[rgb_component] = float(rgb.text)
color = Color(
(rgb_dict["red"], rgb_dict["green"], rgb_dict["blue"]),
format=Color.type_rgb_real,
)
if key == "Ansi 0 Color":
self.black = color
elif key == "Ansi 1 Color":
self.red = color
elif key == "Ansi 2 Color":
self.green = color
elif key == "Ansi 3 Color":
self.yellow = color
elif key == "Ansi 4 Color":
self.blue = color
elif key == "Ansi 5 Color":
self.purple = color
elif key == "Ansi 6 Color":
self.cyan = color
elif key == "Ansi 7 Color":
self.white = color
elif key == "Ansi 8 Color":
self.bright_black = color
elif key == "Ansi 9 Color":
self.bright_red = color
elif key == "Ansi 10 Color":
self.bright_green = color
elif key == "Ansi 11 Color":
self.bright_yellow = color
elif key == "Ansi 12 Color":
self.bright_blue = color
elif key == "Ansi 13 Color":
self.bright_purple = color
elif key == "Ansi 14 Color":
self.bright_cyan = color
elif key == "Ansi 15 Color":
self.bright_white = color
elif key == "Background Color":
self.background_color = color
elif key == "Cursor Color":
self.cursor_color = color
elif key == "Foreground Color":
self.foreground_color = color
elif key == "Selection Color":
self.selection_background = color
return self
def to_wt(self):
return json.dumps(
{
"name": self.name,
"background": self.background_color.get_hex(),
"foreground": self.foreground_color.get_hex(),
"cursorColor": self.cursor_color.get_hex(),
"selectionBackground": self.selection_background.get_hex(),
"black": self.black.get_hex(),
"brightBlack": self.bright_black.get_hex(),
"blue": self.blue.get_hex(),
"brightBlue": self.bright_blue.get_hex(),
"cyan": self.bright_cyan.get_hex(),
"brightCyan": self.bright_cyan.get_hex(),
"green": self.green.get_hex(),
"brightGreen": self.bright_green.get_hex(),
"purple": self.purple.get_hex(),
"brightPurple": self.bright_purple.get_hex(),
"red": self.red.get_hex(),
"brightRed": self.bright_red.get_hex(),
"white": self.white.get_hex(),
"brightWhite": self.bright_white.get_hex(),
"yellow": self.yellow.get_hex(),
"brightYellow": self.bright_yellow.get_hex(),
},
indent=4,
)
def to_mintty(self):
result = ["# Theme: %s" % self.name, ""]
# Write as key/value pairs separated by '='
result.append(f"BackgroundColour={self.background_color.get_rgb_str()}")
result.append(f"ForegroundColour={self.foreground_color.get_rgb_str()}")
result.append(f"CursorColour={self.cursor_color.get_rgb_str()}")
result.append(
f"# SelectionBackground={self.selection_background.get_rgb_str()}"
)
result.append(f"Black={self.black.get_rgb_str()}")
result.append(f"BoldBlack={self.bright_black.get_rgb_str()}")
result.append(f"Blue={self.blue.get_rgb_str()}")
result.append(f"BoldBlue={self.bright_blue.get_rgb_str()}")
result.append(f"Cyan={self.cyan.get_rgb_str()}")
result.append(f"BoldCyan={self.bright_cyan.get_rgb_str()}")
result.append(f"Green={self.green.get_rgb_str()}")
result.append(f"BoldGreen={self.bright_green.get_rgb_str()}")
result.append(f"Magenta={self.purple.get_rgb_str()}")
result.append(f"BoldMagenta={self.bright_purple.get_rgb_str()}")
result.append(f"Red={self.red.get_rgb_str()}")
result.append(f"BoldRed={self.bright_red.get_rgb_str()}")
result.append(f"White={self.white.get_rgb_str()}")
result.append(f"BoldWhite={self.bright_white.get_rgb_str()}")
result.append(f"Yellow={self.yellow.get_rgb_str()}")
result.append(f"BoldYellow={self.bright_yellow.get_rgb_str()}")
return "\n".join(result)
def to_xterm(self):
result = [
"! Theme: %s" % self.name,
"",
"! special",
f"XTerm*foreground: {self.foreground_color.get_hex()}",
f"XTerm*background: {self.background_color.get_hex()}",
f"XTerm*cursorColor: {self.cursor_color.get_hex()}",
"",
"! black",
f"XTerm*color0: {self.black.get_hex()}",
f"XTerm*color8: {self.bright_black.get_hex()}",
"",
"! red",
f"XTerm*color1: {self.red.get_hex()}",
f"XTerm*color9: {self.bright_red.get_hex()}",
"",
"! green",
f"XTerm*color2: {self.green.get_hex()}",
f"XTerm*color10: {self.bright_green.get_hex()}",
"",
"! yellow",
f"XTerm*color3: {self.yellow.get_hex()}",
f"XTerm*color11: {self.bright_yellow.get_hex()}" "",
"! blue",
f"XTerm*color4: {self.blue.get_hex()}",
f"XTerm*color12: {self.bright_blue.get_hex()}",
"",
"! magenta",
f"XTerm*color5: {self.purple.get_hex()}",
f"XTerm*color13: {self.bright_purple.get_hex()}",
"",
"! cyan",
f"XTerm*color6: {self.cyan.get_hex()}",
f"XTerm*color14: {self.bright_cyan.get_hex()}",
"",
"! white",
f"XTerm*color7: {self.white.get_hex()}",
f"XTerm*color15: {self.bright_white.get_hex()}",
]
return "\n".join(result)
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Converts theme files between different formats"
)
parser.add_argument("filename", help='path to file or "-" to read from STDIN')
parser.add_argument(
"-i",
"--input",
help="set format of input theme (wt, mintty, iterm)",
required=True,
type=str,
choices=["wt", "mintty", "iterm"],
)
parser.add_argument(
"-o",
"--output",
help="set format of output theme (wt, mintty, xterm)",
required=True,
type=str,
choices=["wt", "mintty", "xterm"],
)
args = parser.parse_args()
file_contents = ""
if args.filename == "-":
file_contents = sys.stdin.read()
else:
file_path = Path(args.filename).absolute()
if not args.filename or not file_path.exists() or not file_path.is_file():
print("Please provide a valid path to your theme file.")
sys.exit(1)
with open(args.filename, "r", encoding="utf-8") as f:
file_contents = f.read()
theme = None
if args.input == "wt":
theme = Theme().from_wt(file_contents)
elif args.input == "mintty":
theme = Theme().from_mintty(file_contents)
theme.name = file_path.stem
elif args.input == "iterm":
theme = Theme().from_iterm(file_contents)
theme.name = file_path.stem
else:
print(f"Unknown input format: '{args.input}'")
sys.exit(1)
if args.output == "wt":
print(theme.to_wt())
elif args.output == "mintty":
print(theme.to_mintty())
elif args.output == "xterm":
print(theme.to_xterm())
else:
print(f"Unknown output format: '{args.output}'")
sys.exit(1)
@FelisDiligens
Copy link
Author

FelisDiligens commented Jan 20, 2023

$ python convert-theme.py -h
usage: convert-theme.py [-h] -i {wt,mintty,iterm} -o {wt,mintty,xterm} filename

Converts theme files between different formats

positional arguments:
  filename              path to file or "-" to read from STDIN

options:
  -h, --help            show this help message and exit
  -i {wt,mintty,iterm}, --input {wt,mintty,iterm}
                        set format of input theme (wt, mintty, iterm)
  -o {wt,mintty,xterm}, --output {wt,mintty,xterm}
                        set format of output theme (wt, mintty, xterm)

$ python convert-theme.py ./my_wt_theme.json -i wt -o mintty > ./my_mintty_theme

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