From a9af64ac002c9327d99102a19c65750af87eb2e4 Mon Sep 17 00:00:00 2001 From: ylub Date: Thu, 4 Jun 2026 16:42:37 -0500 Subject: [PATCH 1/7] Add sample multi-format exports --- .gitignore | 2 + README.md | 6 + examples/coolors-dark-balanced-sample.conf | 25 ++ .../coolors-dark-balanced-sample.itermcolors | 315 ++++++++++++++++++ examples/coolors-dark-balanced-sample.json | 38 +++ examples/coolors-dark-balanced-sample.lua | 12 + examples/coolors-dark-balanced-sample.toml | 33 ++ 7 files changed, 431 insertions(+) create mode 100644 examples/coolors-dark-balanced-sample.conf create mode 100644 examples/coolors-dark-balanced-sample.itermcolors create mode 100644 examples/coolors-dark-balanced-sample.json create mode 100644 examples/coolors-dark-balanced-sample.lua create mode 100644 examples/coolors-dark-balanced-sample.toml diff --git a/.gitignore b/.gitignore index d517af6..f057554 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ venv/ used_urls.log colors/ *.itermcolors +!examples/ +!examples/*.itermcolors diff --git a/README.md b/README.md index d3008f1..7dee798 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,12 @@ iterm kitty alacritty wezterm data If a target file already exists, the wizard asks before overwriting it. +## Examples + +The `examples/` folder contains one sample palette exported to every supported +format. These files are useful for checking the output shape before importing a +theme into your own terminal. + ## Bright And Sibling Suggestions After ANSI colors are chosen, THEMaker can suggest brighter ANSI colors for the diff --git a/examples/coolors-dark-balanced-sample.conf b/examples/coolors-dark-balanced-sample.conf new file mode 100644 index 0000000..55a5915 --- /dev/null +++ b/examples/coolors-dark-balanced-sample.conf @@ -0,0 +1,25 @@ +# Generated by THEMaker 0.2.0 +# Palette: #25CED1 #FFFFFF #FCEADE #FF8A5B #EA526F +foreground #F8F8F2 +background #101418 +cursor #FFFFFF +cursor_text_color #101418 +selection_foreground #101418 +selection_background #334155 + +color0 #101418 +color1 #EA526F +color2 #25CED1 +color3 #FCEADE +color4 #25CED1 +color5 #FF8A5B +color6 #FFFFFF +color7 #F8F8F2 +color8 #FCEADE +color9 #EA526F +color10 #25CED1 +color11 #FCEADE +color12 #25CED1 +color13 #FF8A5B +color14 #FFFFFF +color15 #F8F8F2 diff --git a/examples/coolors-dark-balanced-sample.itermcolors b/examples/coolors-dark-balanced-sample.itermcolors new file mode 100644 index 0000000..13c9c71 --- /dev/null +++ b/examples/coolors-dark-balanced-sample.itermcolors @@ -0,0 +1,315 @@ + + + + + Ansi 0 Color + + Alpha Component + 1.0 + Blue Component + 0.09411764705882353 + Color Space + sRGB + Green Component + 0.0784313725490196 + Red Component + 0.06274509803921569 + + Ansi 1 Color + + Alpha Component + 1.0 + Blue Component + 0.43529411764705883 + Color Space + sRGB + Green Component + 0.3215686274509804 + Red Component + 0.9176470588235294 + + Ansi 10 Color + + Alpha Component + 1.0 + Blue Component + 0.8196078431372549 + Color Space + sRGB + Green Component + 0.807843137254902 + Red Component + 0.1450980392156863 + + Ansi 11 Color + + Alpha Component + 1.0 + Blue Component + 0.8705882352941177 + Color Space + sRGB + Green Component + 0.9176470588235294 + Red Component + 0.9882352941176471 + + Ansi 12 Color + + Alpha Component + 1.0 + Blue Component + 0.8196078431372549 + Color Space + sRGB + Green Component + 0.807843137254902 + Red Component + 0.1450980392156863 + + Ansi 13 Color + + Alpha Component + 1.0 + Blue Component + 0.3568627450980392 + Color Space + sRGB + Green Component + 0.5411764705882353 + Red Component + 1.0 + + Ansi 14 Color + + Alpha Component + 1.0 + Blue Component + 1.0 + Color Space + sRGB + Green Component + 1.0 + Red Component + 1.0 + + Ansi 15 Color + + Alpha Component + 1.0 + Blue Component + 0.9490196078431372 + Color Space + sRGB + Green Component + 0.9725490196078431 + Red Component + 0.9725490196078431 + + Ansi 2 Color + + Alpha Component + 1.0 + Blue Component + 0.8196078431372549 + Color Space + sRGB + Green Component + 0.807843137254902 + Red Component + 0.1450980392156863 + + Ansi 3 Color + + Alpha Component + 1.0 + Blue Component + 0.8705882352941177 + Color Space + sRGB + Green Component + 0.9176470588235294 + Red Component + 0.9882352941176471 + + Ansi 4 Color + + Alpha Component + 1.0 + Blue Component + 0.8196078431372549 + Color Space + sRGB + Green Component + 0.807843137254902 + Red Component + 0.1450980392156863 + + Ansi 5 Color + + Alpha Component + 1.0 + Blue Component + 0.3568627450980392 + Color Space + sRGB + Green Component + 0.5411764705882353 + Red Component + 1.0 + + Ansi 6 Color + + Alpha Component + 1.0 + Blue Component + 1.0 + Color Space + sRGB + Green Component + 1.0 + Red Component + 1.0 + + Ansi 7 Color + + Alpha Component + 1.0 + Blue Component + 0.9490196078431372 + Color Space + sRGB + Green Component + 0.9725490196078431 + Red Component + 0.9725490196078431 + + Ansi 8 Color + + Alpha Component + 1.0 + Blue Component + 0.8705882352941177 + Color Space + sRGB + Green Component + 0.9176470588235294 + Red Component + 0.9882352941176471 + + Ansi 9 Color + + Alpha Component + 1.0 + Blue Component + 0.43529411764705883 + Color Space + sRGB + Green Component + 0.3215686274509804 + Red Component + 0.9176470588235294 + + Background Color + + Alpha Component + 1.0 + Blue Component + 0.09411764705882353 + Color Space + sRGB + Green Component + 0.0784313725490196 + Red Component + 0.06274509803921569 + + Bold Color + + Alpha Component + 1.0 + Blue Component + 0.9490196078431372 + Color Space + sRGB + Green Component + 0.9725490196078431 + Red Component + 0.9725490196078431 + + Cursor Color + + Alpha Component + 1.0 + Blue Component + 1.0 + Color Space + sRGB + Green Component + 1.0 + Red Component + 1.0 + + Cursor Text Color + + Alpha Component + 1.0 + Blue Component + 0.09411764705882353 + Color Space + sRGB + Green Component + 0.0784313725490196 + Red Component + 0.06274509803921569 + + Foreground Color + + Alpha Component + 1.0 + Blue Component + 0.9490196078431372 + Color Space + sRGB + Green Component + 0.9725490196078431 + Red Component + 0.9725490196078431 + + Selected Text Color + + Alpha Component + 1.0 + Blue Component + 0.09411764705882353 + Color Space + sRGB + Green Component + 0.0784313725490196 + Red Component + 0.06274509803921569 + + Selection Color + + Alpha Component + 1.0 + Blue Component + 0.3333333333333333 + Color Space + sRGB + Green Component + 0.2549019607843137 + Red Component + 0.2 + + TheMaker Family + dark + TheMaker Generator + THEMaker 0.2.0 + TheMaker Mode + balanced + TheMaker Original Palette + #25CED1 #FFFFFF #FCEADE #FF8A5B #EA526F + TheMaker Palette Source + 25ced1 ffffff fceade ff8a5b ea526f + + diff --git a/examples/coolors-dark-balanced-sample.json b/examples/coolors-dark-balanced-sample.json new file mode 100644 index 0000000..eb8a798 --- /dev/null +++ b/examples/coolors-dark-balanced-sample.json @@ -0,0 +1,38 @@ +{ + "generator": "THEMaker 0.2.0", + "name": "coolors-dark-balanced-sample", + "palette": "#25CED1 #FFFFFF #FCEADE #FF8A5B #EA526F", + "palette_source": "25ced1 ffffff fceade ff8a5b ea526f", + "family": "dark", + "family_label": "Dark", + "mode": "balanced", + "colors": { + "background": "101418", + "foreground": "F8F8F2", + "bold": "F8F8F2", + "cursor": "FFFFFF", + "cursor_text": "101418", + "selection": "334155", + "selected_text": "101418", + "ansi": [ + "101418", + "EA526F", + "25CED1", + "FCEADE", + "25CED1", + "FF8A5B", + "FFFFFF", + "F8F8F2" + ], + "bright": [ + "FCEADE", + "EA526F", + "25CED1", + "FCEADE", + "25CED1", + "FF8A5B", + "FFFFFF", + "F8F8F2" + ] + } +} diff --git a/examples/coolors-dark-balanced-sample.lua b/examples/coolors-dark-balanced-sample.lua new file mode 100644 index 0000000..65b4ff3 --- /dev/null +++ b/examples/coolors-dark-balanced-sample.lua @@ -0,0 +1,12 @@ +-- Generated by THEMaker 0.2.0 +-- Palette: #25CED1 #FFFFFF #FCEADE #FF8A5B #EA526F +return { + foreground = "#F8F8F2", + background = "#101418", + cursor_bg = "#FFFFFF", + cursor_fg = "#101418", + selection_bg = "#334155", + selection_fg = "#101418", + ansi = { "#101418", "#EA526F", "#25CED1", "#FCEADE", "#25CED1", "#FF8A5B", "#FFFFFF", "#F8F8F2" }, + brights = { "#FCEADE", "#EA526F", "#25CED1", "#FCEADE", "#25CED1", "#FF8A5B", "#FFFFFF", "#F8F8F2" }, +} diff --git a/examples/coolors-dark-balanced-sample.toml b/examples/coolors-dark-balanced-sample.toml new file mode 100644 index 0000000..010c937 --- /dev/null +++ b/examples/coolors-dark-balanced-sample.toml @@ -0,0 +1,33 @@ +# Generated by THEMaker 0.2.0 +# Palette: #25CED1 #FFFFFF #FCEADE #FF8A5B #EA526F +[colors.primary] +background = "#101418" +foreground = "#F8F8F2" + +[colors.cursor] +text = "#101418" +cursor = "#FFFFFF" + +[colors.selection] +text = "#101418" +background = "#334155" + +[colors.normal] +black = "#101418" +red = "#EA526F" +green = "#25CED1" +yellow = "#FCEADE" +blue = "#25CED1" +magenta = "#FF8A5B" +cyan = "#FFFFFF" +white = "#F8F8F2" + +[colors.bright] +black = "#FCEADE" +red = "#EA526F" +green = "#25CED1" +yellow = "#FCEADE" +blue = "#25CED1" +magenta = "#FF8A5B" +cyan = "#FFFFFF" +white = "#F8F8F2" From 9679d6ace6ed120c0d52b0243df581cddae3bc61 Mon Sep 17 00:00:00 2001 From: ylub Date: Thu, 4 Jun 2026 18:31:58 -0500 Subject: [PATCH 2/7] Show sample words in color previews --- themaker.py | 72 ++++++++++++++++++++++++++++++++++------------------- 1 file changed, 47 insertions(+), 25 deletions(-) diff --git a/themaker.py b/themaker.py index 5ce6142..30b17c3 100644 --- a/themaker.py +++ b/themaker.py @@ -35,6 +35,14 @@ "magenta": "highlight", "cyan": "accent", } +ANSI_ROLE_SAMPLE_WORDS = { + "red": "error", + "green": "success", + "yellow": "warning", + "blue": "command", + "magenta": "highlight", + "cyan": "accent", +} COMMAND_WORDS = {"back", "quit", "exit", "q", "help", "restart"} THEME_FAMILIES = [ @@ -295,10 +303,19 @@ def ansi_fg(hex_color, text): return f"\033[38;2;{r};{g};{b}m{text}\033[0m" +def ansi_text_on(background, foreground, text): + bg_r, bg_g, bg_b = hex_to_rgb(background) + fg_r, fg_g, fg_b = hex_to_rgb(foreground) + return ( + f"\033[48;2;{bg_r};{bg_g};{bg_b}m" + f"\033[38;2;{fg_r};{fg_g};{fg_b}m{text}\033[0m" + ) + + def preview_palette(colors): print("\nPalette:") for i, c in enumerate(colors, 1): - print(f" {i}. #{c} {ansi_bg(c)}") + print(f" {i}. #{c} {ansi_bg(c)} {ansi_fg(c, 'sample text')}") def palette_as_hex(colors): @@ -308,13 +325,13 @@ def palette_as_hex(colors): def preview_palette_choices(colors): print("\nPalette choices:") for i, color in enumerate(colors, 1): - print(f" {i}. #{color} {ansi_bg(color)}") + print(f" {i}. #{color} {ansi_bg(color)} {ansi_fg(color, 'sample text')}") def preview_complementary_choices(options): print("\nComplementary choices:") for i, (name, color) in enumerate(options, 1): - print(f" c{i}. #{color} {ansi_bg(color)} {name}") + print(f" c{i}. #{color} {ansi_bg(color)} {ansi_fg(color, 'sample text')} {name}") def color_choice_label(colors, color): @@ -334,7 +351,7 @@ def preview_families(): def preview_backgrounds(family): print(f"\nSuggested {family['label'].lower()} backgrounds:") for i, (name, color) in enumerate(family["backgrounds"], 1): - print(f" {i}. #{color} {ansi_bg(color)} {name}") + print(f" {i}. #{color} {ansi_bg(color)} {ansi_bg(color, ' sample ')} {name}") def unique_color_options(options): @@ -381,7 +398,10 @@ def preview_foregrounds(options, background, error_color, labels): for index, (name, color) in enumerate(options, 1): error_note = " close to error" if color_distance(color, error_color) < 90 else "" bg_note = " low background contrast" if color_distance(color, background) < 90 else "" - print(f" {index}. #{color} {ansi_fg(color, labels['normal'])} {name}{error_note}{bg_note}") + print( + f" {index}. #{color} {ansi_bg(color)} " + f"{ansi_text_on(background, color, labels['normal'])} {name}{error_note}{bg_note}" + ) def palette_roles(colors, family): @@ -429,28 +449,27 @@ def palette_roles(colors, family): def preview_theme(colors, background, foreground, roles, labels): mapping = [ - ("Background", background), - ("Foreground", foreground), - ("Black / ANSI 0 / base", roles["black"]), - ("Red / ANSI 1 / error", roles["red"]), - ("Green / ANSI 2 / success", roles["green"]), - ("Yellow / ANSI 3 / warning", roles["yellow"]), - ("Blue / ANSI 4 / link", roles["blue"]), - ("Magenta / ANSI 5 / highlight", roles["magenta"]), - ("Cyan / ANSI 6 / accent", roles["cyan"]), - ("White / ANSI 7", foreground), + ("Background", background, ansi_text_on(background, foreground, " sample ")), + ("Foreground", foreground, ansi_text_on(background, foreground, labels["normal"])), + ("Black / ANSI 0 / base", roles["black"], ansi_text_on(background, roles["black"], "base")), + ("Red / ANSI 1 / error", roles["red"], ansi_text_on(background, roles["red"], labels["error"])), + ("Green / ANSI 2 / success", roles["green"], ansi_text_on(background, roles["green"], "success")), + ("Yellow / ANSI 3 / warning", roles["yellow"], ansi_text_on(background, roles["yellow"], labels["warning"])), + ("Blue / ANSI 4 / link", roles["blue"], ansi_text_on(background, roles["blue"], "command")), + ("Magenta / ANSI 5 / highlight", roles["magenta"], ansi_text_on(background, roles["magenta"], "highlight")), + ("Cyan / ANSI 6 / accent", roles["cyan"], ansi_text_on(background, roles["cyan"], labels["accent"])), + ("White / ANSI 7", foreground, ansi_text_on(background, foreground, "normal")), ] print("\nTheme preview:") - for label, color in mapping: - print(f" {label:<28} #{color} {ansi_bg(color)}") + for label, color, sample in mapping: + print(f" {label:<28} #{color} {ansi_bg(color)} {sample}") print("\nText preview:") - print(f" {ansi_bg(background, ' ')} " - f"{ansi_fg(foreground, labels['normal'])} (foreground) " - f"{ansi_fg(roles['cyan'], labels['accent'])} (ANSI 6/cyan/accent) " - f"{ansi_fg(roles['yellow'], labels['warning'])} (ANSI 3/yellow/warning) " - f"{ansi_fg(roles['red'], labels['error'])} (ANSI 1/red/error)") + print(f" {ansi_text_on(background, foreground, labels['normal'])} (foreground) " + f"{ansi_text_on(background, roles['cyan'], labels['accent'])} (ANSI 6/cyan/accent) " + f"{ansi_text_on(background, roles['yellow'], labels['warning'])} (ANSI 3/yellow/warning) " + f"{ansi_text_on(background, roles['red'], labels['error'])} (ANSI 1/red/error)") if color_distance(foreground, roles["red"]) < 90: print(" Warning: normal text is close to the error color.") if color_distance(foreground, background) < 90: @@ -759,9 +778,11 @@ def print_role_mapping(colors, roles): print("\nANSI role mapping:") for role in ("red", "green", "yellow", "blue", "magenta", "cyan"): preview_name = ANSI_ROLE_PREVIEW[role] + sample_word = ANSI_ROLE_SAMPLE_WORDS[role] print( f" ANSI {role:<7} / {preview_name:<17} " - f"#{roles[role]} {ansi_bg(roles[role])} {color_choice_label(colors, roles[role])}" + f"#{roles[role]} {ansi_bg(roles[role])} " + f"{ansi_fg(roles[role], sample_word)} {color_choice_label(colors, roles[role])}" ) @@ -816,9 +837,10 @@ def offer_bright_ansi_suggestions(roles, family): for role in ("red", "green", "yellow", "blue", "magenta", "cyan"): current = roles[role] suggested = suggestions[role] + sample_word = ANSI_ROLE_SAMPLE_WORDS[role] print( - f" {role:<7} #{current} {ansi_bg(current)} -> " - f"#{suggested} {ansi_bg(suggested)}" + f" {role:<7} #{current} {ansi_bg(current)} {ansi_fg(current, sample_word)} -> " + f"#{suggested} {ansi_bg(suggested)} {ansi_fg(suggested, sample_word)}" ) if not confirm_yes("Apply these bright ANSI suggestions? y/n", default=False): From 012700681273ca52ef62f9e568dd8f22a982f924 Mon Sep 17 00:00:00 2001 From: ylub Date: Thu, 4 Jun 2026 18:33:15 -0500 Subject: [PATCH 3/7] Add final ANSI example preview --- themaker.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/themaker.py b/themaker.py index 30b17c3..dfb1ddf 100644 --- a/themaker.py +++ b/themaker.py @@ -470,6 +470,25 @@ def preview_theme(colors, background, foreground, roles, labels): f"{ansi_text_on(background, roles['cyan'], labels['accent'])} (ANSI 6/cyan/accent) " f"{ansi_text_on(background, roles['yellow'], labels['warning'])} (ANSI 3/yellow/warning) " f"{ansi_text_on(background, roles['red'], labels['error'])} (ANSI 1/red/error)") + + print("\nANSI example:") + print( + f" {ansi_text_on(background, roles['blue'], '$ themaker export')} " + f"{ansi_text_on(background, foreground, '--format all')}" + ) + print( + f" {ansi_text_on(background, roles['green'], 'created')} " + f"{ansi_text_on(background, roles['cyan'], 'theme.itermcolors')} " + f"{ansi_text_on(background, roles['magenta'], 'theme.lua')}" + ) + print( + f" {ansi_text_on(background, roles['yellow'], 'warning')} " + f"{ansi_text_on(background, foreground, 'normal text is close to background')}" + ) + print( + f" {ansi_text_on(background, roles['red'], 'error')} " + f"{ansi_text_on(background, foreground, 'invalid hex color')}" + ) if color_distance(foreground, roles["red"]) < 90: print(" Warning: normal text is close to the error color.") if color_distance(foreground, background) < 90: From 86b210012c18dcc4d172d1380a9eff80d6b1a992 Mon Sep 17 00:00:00 2001 From: ylub Date: Thu, 4 Jun 2026 18:39:56 -0500 Subject: [PATCH 4/7] Format script with Black --- themaker.py | 203 ++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 157 insertions(+), 46 deletions(-) diff --git a/themaker.py b/themaker.py index dfb1ddf..086c46d 100644 --- a/themaker.py +++ b/themaker.py @@ -20,7 +20,16 @@ FAMILY_KEY = "TheMaker Family" MODE_KEY = "TheMaker Mode" EXPORT_FORMATS = ("iterm", "kitty", "alacritty", "wezterm", "data") -TERMINAL_ROLE_NAMES = ("black", "red", "green", "yellow", "blue", "magenta", "cyan", "white") +TERMINAL_ROLE_NAMES = ( + "black", + "red", + "green", + "yellow", + "blue", + "magenta", + "cyan", + "white", +) DEFAULT_PREVIEW_LABELS = { "normal": "normal text", "accent": "accent", @@ -165,7 +174,7 @@ def clean_hex(h): def hex_to_rgb(hex_color): h = clean_hex(hex_color) - return tuple(int(h[i:i + 2], 16) for i in (0, 2, 4)) + return tuple(int(h[i : i + 2], 16) for i in (0, 2, 4)) def rgb_to_hex(rgb): @@ -185,18 +194,25 @@ def rgb_plist(hex_color): def plist_rgb_to_hex(value): return rgb_to_hex( - tuple(round(value.get(component, 0) * 255) for component in ( - "Red Component", - "Green Component", - "Blue Component", - )) + tuple( + round(value.get(component, 0) * 255) + for component in ( + "Red Component", + "Green Component", + "Blue Component", + ) + ) ) def parse_palette_input(value): raw_value = value.strip() parsed_url = urlparse(raw_value) - palette_text = parsed_url.path.rstrip("/").split("/")[-1] if parsed_url.scheme or parsed_url.netloc else raw_value + palette_text = ( + parsed_url.path.rstrip("/").split("/")[-1] + if parsed_url.scheme or parsed_url.netloc + else raw_value + ) palette_text = palette_text.replace("-", " ").replace(",", " ") colors = [clean_hex(part) for part in palette_text.split()] @@ -331,7 +347,9 @@ def preview_palette_choices(colors): def preview_complementary_choices(options): print("\nComplementary choices:") for i, (name, color) in enumerate(options, 1): - print(f" c{i}. #{color} {ansi_bg(color)} {ansi_fg(color, 'sample text')} {name}") + print( + f" c{i}. #{color} {ansi_bg(color)} {ansi_fg(color, 'sample text')} {name}" + ) def color_choice_label(colors, color): @@ -396,8 +414,12 @@ def foreground_options(colors, family, background): def preview_foregrounds(options, background, error_color, labels): print("\nSuggested normal text colors:") for index, (name, color) in enumerate(options, 1): - error_note = " close to error" if color_distance(color, error_color) < 90 else "" - bg_note = " low background contrast" if color_distance(color, background) < 90 else "" + error_note = ( + " close to error" if color_distance(color, error_color) < 90 else "" + ) + bg_note = ( + " low background contrast" if color_distance(color, background) < 90 else "" + ) print( f" {index}. #{color} {ansi_bg(color)} " f"{ansi_text_on(background, color, labels['normal'])} {name}{error_note}{bg_note}" @@ -450,14 +472,46 @@ def palette_roles(colors, family): def preview_theme(colors, background, foreground, roles, labels): mapping = [ ("Background", background, ansi_text_on(background, foreground, " sample ")), - ("Foreground", foreground, ansi_text_on(background, foreground, labels["normal"])), - ("Black / ANSI 0 / base", roles["black"], ansi_text_on(background, roles["black"], "base")), - ("Red / ANSI 1 / error", roles["red"], ansi_text_on(background, roles["red"], labels["error"])), - ("Green / ANSI 2 / success", roles["green"], ansi_text_on(background, roles["green"], "success")), - ("Yellow / ANSI 3 / warning", roles["yellow"], ansi_text_on(background, roles["yellow"], labels["warning"])), - ("Blue / ANSI 4 / link", roles["blue"], ansi_text_on(background, roles["blue"], "command")), - ("Magenta / ANSI 5 / highlight", roles["magenta"], ansi_text_on(background, roles["magenta"], "highlight")), - ("Cyan / ANSI 6 / accent", roles["cyan"], ansi_text_on(background, roles["cyan"], labels["accent"])), + ( + "Foreground", + foreground, + ansi_text_on(background, foreground, labels["normal"]), + ), + ( + "Black / ANSI 0 / base", + roles["black"], + ansi_text_on(background, roles["black"], "base"), + ), + ( + "Red / ANSI 1 / error", + roles["red"], + ansi_text_on(background, roles["red"], labels["error"]), + ), + ( + "Green / ANSI 2 / success", + roles["green"], + ansi_text_on(background, roles["green"], "success"), + ), + ( + "Yellow / ANSI 3 / warning", + roles["yellow"], + ansi_text_on(background, roles["yellow"], labels["warning"]), + ), + ( + "Blue / ANSI 4 / link", + roles["blue"], + ansi_text_on(background, roles["blue"], "command"), + ), + ( + "Magenta / ANSI 5 / highlight", + roles["magenta"], + ansi_text_on(background, roles["magenta"], "highlight"), + ), + ( + "Cyan / ANSI 6 / accent", + roles["cyan"], + ansi_text_on(background, roles["cyan"], labels["accent"]), + ), ("White / ANSI 7", foreground, ansi_text_on(background, foreground, "normal")), ] @@ -466,10 +520,12 @@ def preview_theme(colors, background, foreground, roles, labels): print(f" {label:<28} #{color} {ansi_bg(color)} {sample}") print("\nText preview:") - print(f" {ansi_text_on(background, foreground, labels['normal'])} (foreground) " - f"{ansi_text_on(background, roles['cyan'], labels['accent'])} (ANSI 6/cyan/accent) " - f"{ansi_text_on(background, roles['yellow'], labels['warning'])} (ANSI 3/yellow/warning) " - f"{ansi_text_on(background, roles['red'], labels['error'])} (ANSI 1/red/error)") + print( + f" {ansi_text_on(background, foreground, labels['normal'])} (foreground) " + f"{ansi_text_on(background, roles['cyan'], labels['accent'])} (ANSI 6/cyan/accent) " + f"{ansi_text_on(background, roles['yellow'], labels['warning'])} (ANSI 3/yellow/warning) " + f"{ansi_text_on(background, roles['red'], labels['error'])} (ANSI 1/red/error)" + ) print("\nANSI example:") print( @@ -536,7 +592,9 @@ def make_terminal_colors(colors, background, foreground, mode, family, roles): } -def make_theme_model(colors, background, foreground, mode, family, roles, palette_source="", name=""): +def make_theme_model( + colors, background, foreground, mode, family, roles, palette_source="", name="" +): return { "generator": f"{APP_NAME} {APP_VERSION}", "name": name, @@ -545,12 +603,16 @@ def make_theme_model(colors, background, foreground, mode, family, roles, palett "family": family["name"], "family_label": family["label"], "mode": mode, - "colors": make_terminal_colors(colors, background, foreground, mode, family, roles), + "colors": make_terminal_colors( + colors, background, foreground, mode, family, roles + ), } def make_theme(colors, background, foreground, mode, family, roles, palette_source=""): - model = make_theme_model(colors, background, foreground, mode, family, roles, palette_source) + model = make_theme_model( + colors, background, foreground, mode, family, roles, palette_source + ) terminal_colors = model["colors"] theme = { @@ -643,10 +705,14 @@ def write_alacritty_theme(path, model): "", "[colors.normal]", ] - lines.extend(f"{name} = {toml_value(format_hex(color))}" for name, color in normal.items()) + lines.extend( + f"{name} = {toml_value(format_hex(color))}" for name, color in normal.items() + ) lines.append("") lines.append("[colors.bright]") - lines.extend(f"{name} = {toml_value(format_hex(color))}" for name, color in bright.items()) + lines.extend( + f"{name} = {toml_value(format_hex(color))}" for name, color in bright.items() + ) path.write_text("\n".join(lines) + "\n", encoding="utf-8") @@ -829,7 +895,9 @@ def choose_role_color(colors, complement_options, role, current_color): def choose_role_mapping(colors, family, mode, current_roles=None): roles = current_roles.copy() if current_roles else palette_roles(colors, family) print_role_mapping(colors, roles) - customize = ask("Change which palette colors feed the ANSI roles? y/n", "n").strip().lower() + customize = ( + ask("Change which palette colors feed the ANSI roles? y/n", "n").strip().lower() + ) if customize not in {"y", "yes"}: return roles @@ -874,7 +942,9 @@ def offer_bright_ansi_suggestions(roles, family): def preview_label_defaults() -> str: - return " | ".join(DEFAULT_PREVIEW_LABELS[key] for key in ("normal", "accent", "warning", "error")) + return " | ".join( + DEFAULT_PREVIEW_LABELS[key] for key in ("normal", "accent", "warning", "error") + ) def parse_preview_labels(raw_value: str) -> dict: @@ -1074,7 +1144,9 @@ def parallel_theme_suggestions(colors, current_family, current_mode): return suggestions -def offer_parallel_themes(colors, current_family, current_mode, base_name, output_dir, formats): +def offer_parallel_themes( + colors, current_family, current_mode, base_name, output_dir, formats +): suggestions = parallel_theme_suggestions(colors, current_family, current_mode) if not suggestions: return @@ -1135,7 +1207,9 @@ def run_wizard(initial_state=None, start_stage=None, output_dir=OUTPUT_DIR): stage += 1 elif stage == 1: preview_families() - family_choice = choose_number("Choose theme family", len(THEME_FAMILIES), 1) + family_choice = choose_number( + "Choose theme family", len(THEME_FAMILIES), 1 + ) state["family"] = THEME_FAMILIES[family_choice - 1] stage += 1 elif stage == 2: @@ -1148,16 +1222,22 @@ def run_wizard(initial_state=None, start_stage=None, output_dir=OUTPUT_DIR): stage += 1 elif stage == 3: if state.get("background"): - print(f"\nCurrent background: #{state['background']} {ansi_bg(state['background'])}") + print( + f"\nCurrent background: #{state['background']} {ansi_bg(state['background'])}" + ) if confirm_yes("Keep current background? y/n", default=True): stage += 1 continue preview_backgrounds(state["family"]) - bg_choice = choose_number("Choose background", len(state["family"]["backgrounds"]), 1) + bg_choice = choose_number( + "Choose background", len(state["family"]["backgrounds"]), 1 + ) state["background"] = state["family"]["backgrounds"][bg_choice - 1][1] stage += 1 elif stage == 4: - custom_bg = ask("Custom background hex, or press Enter to keep chosen", "") + custom_bg = ask( + "Custom background hex, or press Enter to keep chosen", "" + ) if custom_bg: state["background"] = clean_hex(custom_bg) stage += 1 @@ -1166,8 +1246,12 @@ def run_wizard(initial_state=None, start_stage=None, output_dir=OUTPUT_DIR): stage += 1 elif stage == 6: if state.get("foreground"): - print(f"\nCurrent normal text: #{state['foreground']} {ansi_fg(state['foreground'], state['preview_labels']['normal'])}") - if not confirm_yes("Keep current normal text color? y/n", default=True): + print( + f"\nCurrent normal text: #{state['foreground']} {ansi_fg(state['foreground'], state['preview_labels']['normal'])}" + ) + if not confirm_yes( + "Keep current normal text color? y/n", default=True + ): state["foreground"] = choose_foreground( state["colors"], state["family"], @@ -1189,7 +1273,9 @@ def run_wizard(initial_state=None, start_stage=None, output_dir=OUTPUT_DIR): state["mode"], state.get("roles"), ) - state["roles"] = offer_bright_ansi_suggestions(state["roles"], state["family"]) + state["roles"] = offer_bright_ansi_suggestions( + state["roles"], state["family"] + ) stage += 1 elif stage == 8: preview_theme( @@ -1210,7 +1296,9 @@ def run_wizard(initial_state=None, start_stage=None, output_dir=OUTPUT_DIR): if "formats" not in state: state["formats"] = choose_export_formats() format_labels = ", ".join(state["formats"]) - if not confirm_yes(f"Export this theme as {format_labels}? y/n", default=True): + if not confirm_yes( + f"Export this theme as {format_labels}? y/n", default=True + ): print("Cancelled.") return model = make_theme_model( @@ -1223,7 +1311,9 @@ def run_wizard(initial_state=None, start_stage=None, output_dir=OUTPUT_DIR): state.get("palette_source", ""), state["name"], ) - written, skipped = export_theme_files_interactive(model, output_dir, state["formats"]) + written, skipped = export_theme_files_interactive( + model, output_dir, state["formats"] + ) print_export_results(written, skipped) offer_parallel_themes( state["colors"], @@ -1248,16 +1338,33 @@ def build_parser(): parser = argparse.ArgumentParser( description=f"{APP_NAME}: make terminal color themes from Coolors URLs or hex palettes." ) - parser.add_argument("--palette", help="Coolors URL or hex colors separated by spaces.") - parser.add_argument("--edit", type=Path, help="Start by editing an existing iTerm .itermcolors file.") - parser.add_argument("--out", type=Path, default=OUTPUT_DIR, help="Output directory for exported themes.") + parser.add_argument( + "--palette", help="Coolors URL or hex colors separated by spaces." + ) + parser.add_argument( + "--edit", + type=Path, + help="Start by editing an existing iTerm .itermcolors file.", + ) + parser.add_argument( + "--out", + type=Path, + default=OUTPUT_DIR, + help="Output directory for exported themes.", + ) parser.add_argument( "--format", dest="formats", help="Export formats: all, data, or any of iterm, kitty, alacritty, wezterm, data.", ) - parser.add_argument("--list-themes", action="store_true", help="List existing iTerm themes in the output directory.") - parser.add_argument("--version", action="version", version=f"{APP_NAME} {APP_VERSION}") + parser.add_argument( + "--list-themes", + action="store_true", + help="List existing iTerm themes in the output directory.", + ) + parser.add_argument( + "--version", action="version", version=f"{APP_NAME} {APP_VERSION}" + ) return parser @@ -1281,7 +1388,11 @@ def initial_state_from_args(args): def main(argv=None): args = build_parser().parse_args(argv) if args.list_themes: - themes = sorted(path for path in args.out.glob("*.itermcolors") if path.is_file()) if args.out.exists() else [] + themes = ( + sorted(path for path in args.out.glob("*.itermcolors") if path.is_file()) + if args.out.exists() + else [] + ) if not themes: print(f"No themes found in {args.out}") return From 3b6bcacba2a677fc9960faf8da9b6feb6b0d458f Mon Sep 17 00:00:00 2001 From: ylub Date: Thu, 4 Jun 2026 21:27:21 -0500 Subject: [PATCH 5/7] Add CI for tests --- .github/workflows/tests.yml | 26 ++++++++++++++++++++++++++ .gitignore | 1 + 2 files changed, 27 insertions(+) create mode 100644 .github/workflows/tests.yml diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..3aa6386 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,26 @@ +name: Tests + +on: + pull_request: + push: + branches: + - main + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Check syntax + run: python3 -m py_compile themaker.py test_themaker.py + + - name: Run tests + run: python3 -m unittest diff --git a/.gitignore b/.gitignore index f057554..201f8df 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ __pycache__/ *.py[cod] .venv/ venv/ +.idea/ used_urls.log colors/ *.itermcolors From 25289a21b66679082553ccb8c22df80eb853c2d3 Mon Sep 17 00:00:00 2001 From: ylub Date: Thu, 4 Jun 2026 21:30:33 -0500 Subject: [PATCH 6/7] Document terminal import instructions --- README.md | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/README.md b/README.md index 7dee798..2d3858d 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,45 @@ The `examples/` folder contains one sample palette exported to every supported format. These files are useful for checking the output shape before importing a theme into your own terminal. +## Using Exported Themes + +iTerm2: + +1. Open Settings. +2. Go to Profiles, then Colors. +3. Open Color Presets. +4. Choose Import and select the `.itermcolors` file. + +Kitty: + +```bash +include /path/to/theme.conf +``` + +Add that line to your Kitty config, or copy the generated color lines into it. + +Alacritty: + +```toml +import = ["/path/to/theme.toml"] +``` + +Add that to your Alacritty config, or copy the generated `[colors]` sections +into it. + +WezTerm: + +```lua +local theme = require("theme") + +return { + colors = theme, +} +``` + +Put the generated `.lua` file somewhere WezTerm can require it, or copy the +returned table into your WezTerm config. + ## Bright And Sibling Suggestions After ANSI colors are chosen, THEMaker can suggest brighter ANSI colors for the From 6529ce4be69267cddbaf4d59200536d04304223e Mon Sep 17 00:00:00 2001 From: ylub Date: Thu, 4 Jun 2026 21:32:13 -0500 Subject: [PATCH 7/7] Run CI on feature branch pushes --- .github/workflows/tests.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 3aa6386..6f28a04 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,10 +1,12 @@ name: Tests on: + workflow_dispatch: pull_request: push: branches: - main + - "codex/**" jobs: test: