#!/usr/bin/env python3 """Generate app themes from the Darwin Breeze KDE color scheme. .local/share/color-schemes/DarwinBreezeDark.colors is the only place colors are defined. This script derives from it: .local/share/helium-themes/darwinbreeze/manifest.json .config/ghostty/themes/DarwinBreezeDark t3/darwin-breeze.json (published to T3 Code by apply.sh) """ import configparser import json from pathlib import Path PKG = Path(__file__).resolve().parent SCHEME = PKG / ".local/share/color-schemes/DarwinBreezeDark.colors" HELIUM = PKG / ".local/share/helium-themes/darwinbreeze" GHOSTTY = PKG / ".config/ghostty/themes/DarwinBreezeDark" T3 = PKG / "t3/darwin-breeze.json" WHITE = (255, 255, 255) # Apple systemCyan; the KDE scheme has no cyan role. CYAN = (100, 210, 255) # Apple systemYellow; search highlights in T3 Code. YELLOW = (255, 255, 0) # Breeze draws inactive tabs this far from the window color towards the text # color (measured in Dolphin: 40 -> 62 with text 222). INACTIVE_TAB_MIX = 0.12 def load(): cp = configparser.ConfigParser(interpolation=None, strict=False) cp.optionxform = str cp.read(SCHEME) def color(group, key): return tuple(int(v) for v in cp[f"Colors:{group}"][key].split(",")) return color def mix(a, b, t): return tuple(round(x + (y - x) * t) for x, y in zip(a, b)) def hexc(c): return "#{:02x}{:02x}{:02x}".format(*c) def kwin_compensate(c): """Pre-distort a color for Chromium on KWin. Chromium tags its surfaces as sRGB, KWin shows them with a gamma 2.2 curve, so dark grays come out lighter (40 -> 44). Encoding the target with the inverse makes Helium land on the same values as native apps. """ def ch(v): lin = (v / 255) ** 2.2 enc = 12.92 * lin if lin <= 0.0031308 else 1.055 * lin ** (1 / 2.4) - 0.055 return round(enc * 255) return [ch(v) for v in c] def helium(color): bar = color("Header", "BackgroundNormal") text = color("Window", "ForegroundNormal") view = color("View", "BackgroundNormal") view_text = color("View", "ForegroundNormal") inactive_tab = mix(bar, text, INACTIVE_TAB_MIX) # In Helium's compact layout the active tab and the toolbar segments on # either side share the toolbar color, so toolbar and frame must match # for the strip to read as one bar. Inactive tabs get the lighter Breeze # tab color, the way Dolphin draws them. colors = { "frame": bar, "frame_inactive": bar, "frame_incognito": bar, "frame_incognito_inactive": bar, "toolbar": bar, "background_tab": inactive_tab, "background_tab_inactive": inactive_tab, "tab_text": text, "tab_background_text": text, "tab_background_text_inactive": color("Window", "ForegroundInactive"), "toolbar_text": text, "toolbar_button_icon": text, "bookmark_text": text, "omnibox_background": view, "omnibox_text": view_text, "ntp_background": view, "ntp_text": view_text, "ntp_link": color("View", "ForegroundLink"), } return {k: kwin_compensate(v) for k, v in colors.items()} def ghostty(color): bg = color("View", "BackgroundNormal") fg = color("View", "ForegroundNormal") accent = color("Selection", "BackgroundNormal") accent_text = color("Selection", "ForegroundNormal") normal = [ mix(bg, fg, 0.15), color("View", "ForegroundNegative"), color("View", "ForegroundPositive"), color("View", "ForegroundNeutral"), color("View", "ForegroundLink"), color("View", "ForegroundVisited"), CYAN, fg, ] bright = [ mix(bg, fg, 0.45), *(mix(c, WHITE, 0.25) for c in normal[1:7]), mix(fg, WHITE, 0.6), ] lines = [ f"# Generated by darwin-breeze/build.py from {SCHEME.name}", f"background = {hexc(bg)}", f"foreground = {hexc(fg)}", f"cursor-color = {hexc(accent)}", f"cursor-text = {hexc(accent_text)}", f"selection-background = {hexc(accent)}", f"selection-foreground = {hexc(accent_text)}", ] lines += [f"palette = {i}={hexc(c)}" for i, c in enumerate(normal + bright)] return "\n".join(lines) + "\n" def t3(color): """Every color slot of a T3 Code theme, mapped onto a scheme role.""" window, window_text = color("Window", "BackgroundNormal"), color("Window", "ForegroundNormal") view, view_text = color("View", "BackgroundNormal"), color("View", "ForegroundNormal") header, header_text = color("Header", "BackgroundNormal"), color("Header", "ForegroundNormal") button, button_text = color("Button", "BackgroundNormal"), color("Button", "ForegroundNormal") accent, accent_text = color("Selection", "BackgroundNormal"), color("Selection", "ForegroundNormal") unfocused = color("Selection][Inactive", "BackgroundNormal") unfocused_text = color("Selection][Inactive", "ForegroundNormal") muted = color("View", "ForegroundInactive") negative = color("View", "ForegroundNegative") neutral = color("View", "ForegroundNeutral") link = color("View", "ForegroundLink") window_line = mix(window, window_text, 0.115) colors = { "canvas": view, "chrome": header, "toolbar": header, "toolbarForeground": header_text, "toolbarBorder": mix(header, header_text, 0.115), "toolbarControl": button, "toolbarControlForeground": button_text, "toolbarControlHover": color("Button", "BackgroundAlternate"), "surface": view, "surfaceRaised": window, "surfaceOverlay": window, "text": view_text, "textMuted": muted, "border": mix(view, view_text, 0.115), "input": mix(view, view_text, 0.3), "focus": mix(view, accent, 0.5), "accent": accent, "accentForeground": accent_text, "secondary": button, "secondaryForeground": button_text, "muted": window, "mutedForeground": color("Window", "ForegroundInactive"), "placeholder": muted, "secondaryLabel": muted, "iconMuted": muted, "error": negative, "errorForeground": negative, "errorSurface": mix(view, negative, 0.1), "warning": neutral, "warningForeground": neutral, "warningSurface": mix(view, neutral, 0.1), "update": link, "updateForeground": link, "updateSurface": mix(view, link, 0.12), "accentSurface": unfocused, "accentSurfaceForeground": unfocused_text, "messageSurface": window, "messageForeground": window_text, "messageAction": accent, "messageActionForeground": accent_text, "messageActionHover": color("Selection", "BackgroundAlternate"), "codeBackground": window, "codeForeground": WHITE, "searchMatchBackground": mix(view, YELLOW, 0.25), "searchMatchForeground": view_text, "searchMatchActiveBackground": YELLOW, "searchMatchActiveForeground": (0, 0, 0), "sidebar": window, "sidebarForeground": window_text, "sidebarMutedForeground": color("Window", "ForegroundInactive"), "sidebarControlSurface": button, "sidebarRowHover": window_line, "sidebarRowActive": unfocused, "sidebarRowSelected": unfocused, "sidebarBorder": window_line, "terminalBackground": view, "terminalForeground": WHITE, "terminalCursor": WHITE, "terminalSelection": mix(view, accent, 0.4), "terminalScrollbar": mix(view, view_text, 0.3), "terminalScrollbarHover": muted, } # T3 Code runs on Chromium too, so it needs the same KWin compensation. return {k: hexc(kwin_compensate(v)) for k, v in colors.items()} def write(path, text): path.parent.mkdir(parents=True, exist_ok=True) if path.exists() and path.read_text() == text: return False path.write_text(text) print(f"wrote {path.relative_to(PKG)}") return True def main(): color = load() manifest = { "manifest_version": 3, "name": "Darwin Breeze", "version": "1.0", "description": f"Generated by darwin-breeze/build.py from {SCHEME.name}", "theme": {"colors": helium(color)}, } if write(HELIUM / "manifest.json", json.dumps(manifest, indent=2) + "\n"): # Helium would keep showing the old colors from its cache. (HELIUM / "Cached Theme.pak").unlink(missing_ok=True) write(GHOSTTY, ghostty(color)) theme = {"version": 1, "name": "Darwin Breeze", "appearance": "dark", "colors": t3(color)} write(T3, json.dumps(theme, indent=2) + "\n") if __name__ == "__main__": main()