diff --git a/README.md b/README.md index 27b97ed..8a0432c 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ It currently exports: - Kitty `.conf` - Alacritty `.toml` - WezTerm `.lua` +- CotEditor `.cottheme` - Source `.yaml` for terminal color scheme repos - Portable JSON theme data for someone else or another tool to export later @@ -93,7 +94,7 @@ At the save step, THEMaker asks what to export: Supported format names are: ```text -iterm terminal kitty alacritty wezterm yaml data +iterm terminal kitty alacritty wezterm coteditor yaml data ``` | Format | Extension | Use | @@ -103,6 +104,7 @@ iterm terminal kitty alacritty wezterm yaml data | `kitty` | `.conf` | Include or copy into Kitty config. | | `alacritty` | `.toml` | Import or copy into Alacritty config. | | `wezterm` | `.lua` | Require or copy into WezTerm config. | +| `coteditor` | `.cottheme` | Import into CotEditor. | | `yaml` | `.yaml` | Submit source schemes to terminal color scheme repos. | | `data` | `.json` | Save portable theme data for another tool or maintainer. | @@ -166,6 +168,11 @@ return { Put the generated `.lua` file somewhere WezTerm can require it, or copy the returned table into your WezTerm config. +CotEditor: + +Open the generated `.cottheme` file, or import it from CotEditor's Appearance +settings. + YAML: The `.yaml` export follows the source scheme guidance from @@ -210,8 +217,7 @@ python3 -m unittest ## Releases -The first public release should be tagged as `v0.2.0` after the public export -branch is merged. +Public releases are tagged on GitHub. The first public release is `v0.2.0`. ## Credits diff --git a/examples/coolors-dark-balanced-sample.cottheme b/examples/coolors-dark-balanced-sample.cottheme new file mode 100644 index 0000000..68ab923 --- /dev/null +++ b/examples/coolors-dark-balanced-sample.cottheme @@ -0,0 +1,63 @@ +{ + "attributes": { + "color": "#FFFFFF" + }, + "background": { + "color": "#101418" + }, + "characters": { + "color": "#FCEADE" + }, + "commands": { + "color": "#FFFFFF" + }, + "comments": { + "color": "#909190" + }, + "highlight": { + "color": "#FCEADE", + "usesSystemSetting": false + }, + "insertionPoint": { + "color": "#FFFFFF", + "usesSystemSetting": false + }, + "invisibles": { + "color": "#616464" + }, + "keywords": { + "color": "#25CED1" + }, + "lineHighlight": { + "color": "#222A3680" + }, + "metadata": { + "author": "@ylub", + "description": "Generated by THEMaker 0.3.0.", + "distributionURL": "https://github.com/ylub/themaker", + "license": "MIT" + }, + "name": "coolors-dark-balanced-sample", + "numbers": { + "color": "#FF8A5B" + }, + "selection": { + "color": "#334155", + "usesSystemSetting": false + }, + "strings": { + "color": "#25CED1" + }, + "text": { + "color": "#F8F8F2" + }, + "types": { + "color": "#FCEADE" + }, + "values": { + "color": "#FF8A5B" + }, + "variables": { + "color": "#F8F8F2" + } +} diff --git a/test_themaker.py b/test_themaker.py index 3b94d74..24ae337 100644 --- a/test_themaker.py +++ b/test_themaker.py @@ -1,3 +1,4 @@ +import json import plistlib import tempfile import unittest @@ -86,15 +87,28 @@ def test_exporters_write_expected_files(self): written, skipped = themaker.export_theme_files( model, out_dir, - ["iterm", "terminal", "kitty", "alacritty", "wezterm", "yaml", "data"], + [ + "iterm", + "terminal", + "kitty", + "alacritty", + "wezterm", + "coteditor", + "yaml", + "data", + ], ) self.assertEqual(skipped, []) - self.assertEqual(len(written), 7) + self.assertEqual(len(written), 8) self.assertTrue((out_dir / "sample.itermcolors").exists()) self.assertTrue((out_dir / "sample.terminal").exists()) self.assertIn("foreground #F8F8F2", (out_dir / "sample.conf").read_text()) self.assertIn("[colors.primary]", (out_dir / "sample.toml").read_text()) self.assertIn("return {", (out_dir / "sample.lua").read_text()) + cottheme = json.loads((out_dir / "sample.cottheme").read_text()) + self.assertEqual(cottheme["background"]["color"], "#101418") + self.assertEqual(cottheme["text"]["color"], "#F8F8F2") + self.assertEqual(cottheme["metadata"]["author"], "@ylub") yaml_text = (out_dir / "sample.yaml").read_text() self.assertIn('color_01: "#101418"', yaml_text) self.assertIn('color_16: "#FFFFFF"', yaml_text) diff --git a/themaker.py b/themaker.py index 8ae33c3..505e8c3 100644 --- a/themaker.py +++ b/themaker.py @@ -9,7 +9,7 @@ from pathlib import Path APP_NAME = "THEMaker" -APP_VERSION = "0.2.0" +APP_VERSION = "0.3.0" APP_AUTHOR = "@ylub" PROJECT_URL = "https://github.com/ylub/themaker" COOLORS_URL = "https://coolors.co" @@ -28,6 +28,7 @@ "kitty", "alacritty", "wezterm", + "coteditor", "yaml", "data", ) @@ -296,6 +297,18 @@ def color_distance(first_hex, second_hex): return sum((a - b) ** 2 for a, b in zip(first, second)) ** 0.5 +def blend_colors(first_hex, second_hex, second_weight): + first = hex_to_rgb(first_hex) + second = hex_to_rgb(second_hex) + first_weight = 1 - second_weight + return rgb_to_hex( + tuple( + round(first_channel * first_weight + second_channel * second_weight) + for first_channel, second_channel in zip(first, second) + ) + ) + + def complementary_color(hex_color, family, mode): r, g, b = (channel / 255 for channel in hex_to_rgb(hex_color)) hue, lightness, saturation = colorsys.rgb_to_hls(r, g, b) @@ -931,6 +944,56 @@ def write_gogh_yaml_theme(path, model): path.write_text("\n".join(lines) + "\n", encoding="utf-8") +def cottheme_color(hex_color): + return {"color": format_hex(hex_color)} + + +def write_coteditor_theme(path, model): + colors = model["colors"] + normal = dict(zip(TERMINAL_ROLE_NAMES, colors["ansi"])) + muted = blend_colors(colors["background"], colors["foreground"], 0.55) + invisibles = blend_colors(colors["background"], colors["foreground"], 0.35) + line_highlight = blend_colors(colors["background"], colors["selection"], 0.5) + theme = { + "attributes": cottheme_color(normal["cyan"]), + "background": cottheme_color(colors["background"]), + "characters": cottheme_color(normal["yellow"]), + "commands": cottheme_color(normal["cyan"]), + "comments": cottheme_color(muted), + "highlight": { + "color": format_hex(normal["yellow"]), + "usesSystemSetting": False, + }, + "insertionPoint": { + "color": format_hex(colors["cursor"]), + "usesSystemSetting": False, + }, + "invisibles": cottheme_color(invisibles), + "keywords": cottheme_color(normal["blue"]), + "lineHighlight": { + "color": f"#{line_highlight}80", + }, + "metadata": { + "author": APP_AUTHOR, + "description": f"Generated by {model['generator']}.", + "distributionURL": PROJECT_URL, + "license": "MIT", + }, + "name": model["name"] or "THEMaker Theme", + "numbers": cottheme_color(normal["magenta"]), + "selection": { + "color": format_hex(colors["selection"]), + "usesSystemSetting": False, + }, + "strings": cottheme_color(normal["green"]), + "text": cottheme_color(colors["foreground"]), + "types": cottheme_color(normal["yellow"]), + "values": cottheme_color(normal["magenta"]), + "variables": cottheme_color(colors["foreground"]), + } + path.write_text(json.dumps(theme, indent=2) + "\n", encoding="utf-8") + + def lua_string(value): return json.dumps(value) @@ -968,6 +1031,7 @@ def write_theme_data(path, model): "kitty": (".conf", write_kitty_theme), "alacritty": (".toml", write_alacritty_theme), "wezterm": (".lua", write_wezterm_theme), + "coteditor": (".cottheme", write_coteditor_theme), "yaml": (".yaml", write_gogh_yaml_theme), "data": (".json", write_theme_data), } @@ -1221,6 +1285,9 @@ def parse_export_formats(raw_value): "alac": "alacritty", "w": "wezterm", "wez": "wezterm", + "cot": "coteditor", + "cotedit": "coteditor", + "cottheme": "coteditor", "y": "yaml", "gogh": "yaml", "iterm-yaml": "yaml", @@ -1244,12 +1311,12 @@ def parse_export_formats(raw_value): def choose_export_formats(): print("\nExport options:") print( - " all iTerm2, macOS Terminal, Kitty, Alacritty, WezTerm, YAML, and data JSON" + " all iTerm2, macOS Terminal, Kitty, Alacritty, WezTerm, CotEditor, YAML, and data JSON" ) print(" one Choose one format") print(" some Choose a few formats") print(" data Save portable JSON data for someone else to export") - print("Formats: iterm, terminal, kitty, alacritty, wezterm, yaml, data") + print("Formats: iterm, terminal, kitty, alacritty, wezterm, coteditor, yaml, data") while True: raw = ask("Export formats", "all") if raw.strip().lower() == "one": @@ -1584,7 +1651,7 @@ def build_parser(): parser.add_argument( "--format", dest="formats", - help="Export formats: all, data, or any of iterm, terminal, kitty, alacritty, wezterm, yaml, data.", + help="Export formats: all, data, or any of iterm, terminal, kitty, alacritty, wezterm, coteditor, yaml, data.", ) parser.add_argument( "--list-themes",