#!/usr/bin/env python3 """Generate the Ash Omarchy themes (ash-light / ash-dark) from the Harbor reference themes in tools/reference/, using the Ash palette defined for TriliumNext in trilium-themes/ash-{light,dark}.css. Usage: python3 tools/recolor.py """ import math import re import sys from pathlib import Path ASH_DARK = { "void": "100f12", "panel": "1c1b1f", "raised": "29282c", "border": "3a383d", "mid": "54514f", "ash": "6d6a63", "stone": "999187", "bone": "e6dfd1", "chalk": "f3ede1", "icon": "b0a89a", "ember": "e0914f", "gold": "d6b46a", "blood": "c15c46", "rot": "b47e99", "moss": "8f9a5c", "steel": "7c9aa1", "steel_light": "a3bcc1", "violet": "a585ac", "rust": "a86a48", } ASH_LIGHT = { "paper": "e9e3d5", "raised": "f2ecdf", "deep": "dcd5c3", "border": "c9c1ad", "border2": "b7ae98", "muted": "6f685c", "icon": "5b5449", "grey": "8d8578", "grey2": "a89e85", "ink": "2a2822", "ink_dark": "141310", "ember": "a85f1d", "ember_bright": "c56a2b", "gold": "8a6a24", "blood": "96392a", "blood_bright": "b04331", "blood_dark": "7a2f24", "rot": "7c4a66", "moss": "5c6631", "steel": "3d6b72", "violet": "6a4c72", "rust": "7d4a2c", } MAP_DARK = { "1b1b1b": "1c1b1f", "141414": "100f12", "191919": "100f12", "1f1f1f": "100f12", "323232": "29282c", "383838": "3a383d", "4c4c4c": "54514f", "6d6d6d": "54514f", "888888": "6d6a63", "929292": "6d6a63", "b6b6b6": "999187", "87856d": "999187", "817f68": "6d6a63", "9a9880": "999187", "efebdc": "e6dfd1", "f4f2e8": "f3ede1", "e1ce98": "e6dfd1", "e75a50": "e0914f", "df7e76": "e0914f", "ff7783": "c15c46", "f44336": "c15c46", "d83a42": "c15c46", "d83a41": "c15c46", "e58980": "b47e99", "a99b7a": "d6b46a", "c0af7f": "d6b46a", "e67e22": "e0914f", "cc954c": "d6b46a", "ca9654": "d6b46a", "9683a1": "a585ac", "77838a": "7c9aa1", "77868a": "7c9aa1", "9ba2a6": "a3bcc1", "99a4af": "a3bcc1", "b3b9bc": "b0a89a", } MAP_LIGHT = { "dfe4c4": "e9e3d5", "d3d9b0": "dcd5c3", "c8d4a8": "dcd5c3", "ecebbd": "f2ecdf", "d8d9b8": "dcd5c3", "bec2a7": "c9c1ad", "1c2d28": "2a2822", "253631": "141310", "2a2a2a": "2a2822", "383838": "2a2822", "384f54": "5b5449", "7d8794": "8d8578", "83838b": "8d8578", "82838b": "8d8578", "858f8b": "8d8578", "acb3b1": "a89e85", "606c68": "6f685c", "5e81ac": "a85f1d", "4c6c94": "3d6b72", "476489": "3d6b72", "386593": "3d6b72", "3a6a9a": "3d6b72", "3c6499": "3d6b72", "316a7b": "3d6b72", "286b75": "3d6b72", "256b7a": "3d6b72", "3d727d": "3d6b72", "b14752": "96392a", "a4424c": "7a2f24", "9d2e3a": "7a2f24", "b03743": "96392a", "d83a42": "b04331", "d83a41": "b04331", "ff0000": "96392a", "556753": "5c6631", "43a25a": "5c6631", "43a258": "5c6631", "40a258": "5c6631", "4a6a4a": "5c6631", "466b46": "5c6631", "dc8164": "c56a2b", "8a5b81": "6a4c72", "7f5578": "7c4a66", "835665": "7c4a66", "5a4a7a": "6a4c72", "695998": "6a4c72", "cc954c": "8a6a24", "ca9654": "8a6a24", "8f5424": "7d4a2c", "9d4b25": "7d4a2c", "955121": "7d4a2c", # 3-digit shorthand (vscode symbol icons) "f0f": "6a4c72", } VARIANTS = { "light": {"ash": ASH_LIGHT, "out": "ash-light", "map": MAP_LIGHT, "post": []}, "dark": {"ash": ASH_DARK, "out": "ash-dark", "map": MAP_DARK, "post": [("Ashdark", "Ash Dark"), ("ashdark", "ash-dark")]}, } NAME_REPLACEMENTS = [("HARBOR", "ASH"), ("Harbor", "Ash"), ("harbor", "ash")] HEX8 = re.compile(r"#([0-9a-fA-F]{6})([0-9a-fA-F]{2})\b") HEX6 = re.compile(r"#([0-9a-fA-F]{6})\b") HEX3 = re.compile(r"#([0-9a-fA-F]{3})\b") RGBA_DEC = re.compile(r"(rgba?\(\s*)(\d{1,3})(\s*,\s*)(\d{1,3})(\s*,\s*)(\d{1,3})(\s*,[^)])") RGBA_HEX = re.compile(r"(? 150: self.unmapped[h] = self.unmapped.get(h, 0) + 1 return None return nearest def fix_case(self, out, ref): return out.upper() if ref != ref.lower() else out def text(self, s): s = HEX8.sub(lambda m: "#" + self.fix_case(self.resolve(m[1]) or m[1], m[1]) + m[2], s) def h6(m): r = self.resolve(m[1]) return "#" + self.fix_case(r, m[1]) if r else m[0] s = HEX6.sub(h6, s) def h3(m): r = self.resolve(m[1]) or self.resolve(m[1] * 2) return "#" + self.fix_case(r, m[1]) if r else m[0] s = HEX3.sub(h3, s) def dec(m): key = f"{int(m[2]):02x}{int(m[4]):02x}{int(m[6]):02x}" r = self.resolve(key) if not r: return m[0] t = rgb(r) return f"{m[1]}{t[0]}{m[3]}{t[1]}{m[5]}{t[2]}{m[7]}" s = RGBA_DEC.sub(dec, s) # hyprland bare rgba(rrggbbaa) tokens (no # prefix) s = re.sub(r"(rgba\()([0-9a-fA-F]{6})([0-9a-fA-F]{2})(\))", lambda m: m[1] + self.fix_case(self.resolve(m[2]) or m[2], m[2]) + m[3] + m[4], s) # hyprland bare rgb(rrggbb) tokens (no # prefix) s = re.sub(r"(rgba?\()([0-9a-fA-F]{6})(\))", lambda m: m[1] + self.fix_case(self.resolve(m[2]) or m[2], m[2]) + m[3], s) for old, new in NAME_REPLACEMENTS: s = s.replace(old, new) for old, new in self.post: s = s.replace(old, new) return s def main(): root = Path(sys.argv[1]).resolve() total_unmapped = {} for variant, cfg in VARIANTS.items(): ref_dir = root / "tools/reference" / variant out_dir = root / cfg["out"] recolorer = Recolorer(cfg["map"], cfg["ash"], cfg.get("post", ())) for src in sorted(ref_dir.rglob("*")): if not src.is_file(): continue rel = src.relative_to(ref_dir) name = re.sub(r"(?i)harbordark", "ash-dark", str(rel)) name = re.sub(r"(?i)harbor", "ash", name) dst = out_dir / name dst.parent.mkdir(parents=True, exist_ok=True) data = src.read_text(encoding="utf-8") out = recolorer.text(data) if rel.name == "icons.theme": out = "Yaru" if variant == "light" else "Yaru-dark" dst.write_text(out, encoding="utf-8") for k, v in recolorer.unmapped.items(): total_unmapped[f"{variant}:{k}"] = v print(f"{cfg['out']}: generated") if total_unmapped: print("left unchanged (no good Ash equivalent):") for k, v in sorted(total_unmapped.items(), key=lambda x: -x[1]): print(f" {k} x{v}") if __name__ == "__main__": main()