feat(darwin-breeze): unify theme colors across KDE, Helium, T3 Code and Ghostty
Move the Darwin Breeze Dark color scheme and global theme into a stow package. build.py generates the Helium, T3 Code and Ghostty themes from DarwinBreezeDark.colors, and apply.sh rebuilds and applies them, so the colors are edited in one place. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d215693df4
commit
75b1383edf
14 files changed
+798
-1
No files matched your search
Executable
+250
@@ -0,0 +1,250 @@
|
||||
#!/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()
|
||||
Reference in new issue
Block a user