feat(darwin-breeze): add VS Code and Telegram themes
Generate and install a VS Code theme and build an importable Telegram theme from the color scheme.
This commit is contained in:
1 parent
75b1383edf
commit
26a29b3182
14 files changed
+6613
-15
No files matched your search
+441
-9
@@ -7,10 +7,18 @@ 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)
|
||||
telegram/DarwinBreezeDark.tdesktop-theme (not committed; imported into Telegram by hand)
|
||||
vscode/ (packed into vscode/darwin-breeze.vsix, installed by apply.sh)
|
||||
"""
|
||||
|
||||
import colorsys
|
||||
import configparser
|
||||
import io
|
||||
import json
|
||||
import re
|
||||
import struct
|
||||
import zipfile
|
||||
import zlib
|
||||
from pathlib import Path
|
||||
|
||||
PKG = Path(__file__).resolve().parent
|
||||
@@ -18,6 +26,20 @@ 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"
|
||||
# Telegram Desktop's built-in night theme, used as the base for every key the
|
||||
# scheme has no role for: the colors file from telegramdesktop/tdesktop at
|
||||
# 837b256, Telegram/Resources/night.tdesktop-theme.
|
||||
TELEGRAM_BASE = PKG / "telegram/night.tdesktop-palette"
|
||||
TELEGRAM = PKG / "telegram/DarwinBreezeDark.tdesktop-theme"
|
||||
VSCODE = PKG / "vscode"
|
||||
VSCODE_THEME = "themes/DarwinBreezeDark-color-theme.json"
|
||||
# VS Code's default dark theme, Dark 2026 (which includes Dark Modern, Dark+
|
||||
# and Visual Studio Dark), from Code - OSS 1.138 extensions/theme-defaults.
|
||||
# The workbench colors start from it.
|
||||
VSCODE_UI = PKG / "vscode/base/2026-dark.json"
|
||||
# Pierre Dark, the syntax theme T3 Code highlights code with (from T3 Code
|
||||
# 0.1.4). The editor uses its token colors as they are.
|
||||
VSCODE_SYNTAX = PKG / "vscode/base/pierre-dark.json"
|
||||
|
||||
WHITE = (255, 255, 255)
|
||||
|
||||
@@ -48,7 +70,8 @@ def mix(a, b, t):
|
||||
|
||||
|
||||
def hexc(c):
|
||||
return "#{:02x}{:02x}{:02x}".format(*c)
|
||||
"""#rrggbb, or #rrggbbaa when c carries an alpha."""
|
||||
return "#" + "".join(f"{v:02x}" for v in c)
|
||||
|
||||
|
||||
def kwin_compensate(c):
|
||||
@@ -101,12 +124,10 @@ def helium(color):
|
||||
return {k: kwin_compensate(v) for k, v in colors.items()}
|
||||
|
||||
|
||||
def ghostty(color):
|
||||
def ansi(color):
|
||||
"""The 16 terminal colors, shared by Ghostty and the VS Code terminal."""
|
||||
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"),
|
||||
@@ -122,6 +143,14 @@ def ghostty(color):
|
||||
*(mix(c, WHITE, 0.25) for c in normal[1:7]),
|
||||
mix(fg, WHITE, 0.6),
|
||||
]
|
||||
return normal + bright
|
||||
|
||||
|
||||
def ghostty(color):
|
||||
bg = color("View", "BackgroundNormal")
|
||||
fg = color("View", "ForegroundNormal")
|
||||
accent = color("Selection", "BackgroundNormal")
|
||||
accent_text = color("Selection", "ForegroundNormal")
|
||||
|
||||
lines = [
|
||||
f"# Generated by darwin-breeze/build.py from {SCHEME.name}",
|
||||
@@ -132,7 +161,7 @@ def ghostty(color):
|
||||
f"selection-background = {hexc(accent)}",
|
||||
f"selection-foreground = {hexc(accent_text)}",
|
||||
]
|
||||
lines += [f"palette = {i}={hexc(c)}" for i, c in enumerate(normal + bright)]
|
||||
lines += [f"palette = {i}={hexc(c)}" for i, c in enumerate(ansi(color))]
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
@@ -218,11 +247,404 @@ def t3(color):
|
||||
return {k: hexc(kwin_compensate(v)) for k, v in colors.items()}
|
||||
|
||||
|
||||
def write(path, text):
|
||||
def telegram(color):
|
||||
"""Telegram keys mapped onto a scheme role; the rest come from night."""
|
||||
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")
|
||||
title_inactive = color("Header][Inactive", "BackgroundNormal")
|
||||
title_inactive_text = color("Header][Inactive", "ForegroundNormal")
|
||||
tooltip, tooltip_text = color("Tooltip", "BackgroundNormal"), color("Tooltip", "ForegroundNormal")
|
||||
accent, accent_text = color("Selection", "BackgroundNormal"), color("Selection", "ForegroundNormal")
|
||||
accent_hover = color("Selection", "BackgroundAlternate")
|
||||
muted = color("Window", "ForegroundInactive")
|
||||
negative = color("Window", "ForegroundNegative")
|
||||
positive = color("Window", "ForegroundPositive")
|
||||
link = color("Window", "ForegroundLink")
|
||||
hover = mix(window, window_text, 0.115)
|
||||
ripple = mix(window, window_text, 0.18)
|
||||
control = mix(window, window_text, 0.3)
|
||||
msg_in = mix(view, view_text, 0.1)
|
||||
msg_out = mix(view, accent, 0.5)
|
||||
|
||||
return {
|
||||
"windowBg": window,
|
||||
"windowFg": window_text,
|
||||
"windowBgOver": hover,
|
||||
"windowBgRipple": ripple,
|
||||
"windowFgOver": window_text,
|
||||
"windowSubTextFg": muted,
|
||||
"windowSubTextFgOver": mix(muted, window_text, 0.2),
|
||||
"windowBoldFg": window_text,
|
||||
"windowBoldFgOver": window_text,
|
||||
"windowBgActive": accent,
|
||||
"windowFgActive": accent_text,
|
||||
"windowActiveTextFg": link,
|
||||
"windowShadowFgFallback": window,
|
||||
"activeButtonBg": accent,
|
||||
"activeButtonBgOver": accent_hover,
|
||||
"activeButtonBgRipple": mix(accent, WHITE, 0.2),
|
||||
"activeButtonFg": accent_text,
|
||||
"activeButtonFgOver": accent_text,
|
||||
"activeButtonSecondaryFg": mix(accent, accent_text, 0.75),
|
||||
"activeLineFg": accent,
|
||||
"activeLineFgError": negative,
|
||||
"lightButtonBg": window,
|
||||
"lightButtonBgOver": mix(window, accent, 0.12),
|
||||
"lightButtonBgRipple": mix(window, accent, 0.2),
|
||||
"lightButtonFg": link,
|
||||
"attentionButtonFg": negative,
|
||||
"attentionButtonFgOver": negative,
|
||||
"attentionButtonBgOver": mix(window, negative, 0.12),
|
||||
"attentionButtonBgRipple": mix(window, negative, 0.2),
|
||||
"outlineButtonOutlineFg": accent,
|
||||
"menuBg": window,
|
||||
"menuBgOver": hover,
|
||||
"menuBgRipple": ripple,
|
||||
"menuIconFg": muted,
|
||||
"menuIconFgOver": mix(muted, window_text, 0.3),
|
||||
"menuSubmenuArrowFg": muted,
|
||||
"menuFgDisabled": control,
|
||||
"menuSeparatorFg": hover,
|
||||
"checkboxFg": muted,
|
||||
"sliderBgInactive": control,
|
||||
"sliderBgActive": accent,
|
||||
"inputBorderFg": control,
|
||||
# Breeze draws text fields in the view color with a blue focus ring.
|
||||
"filterInputActiveBg": view,
|
||||
"filterInputInactiveBg": view,
|
||||
"filterInputBorderFg": accent,
|
||||
"tooltipBg": tooltip,
|
||||
"tooltipFg": tooltip_text,
|
||||
"tooltipBorderFg": mix(tooltip, tooltip_text, 0.115),
|
||||
"titleBg": title_inactive,
|
||||
"titleBgActive": header,
|
||||
"titleFg": title_inactive_text,
|
||||
"titleFgActive": header_text,
|
||||
"topBarBg": header,
|
||||
"boxBg": window,
|
||||
"boxTitleFg": window_text,
|
||||
"boxTextFgGood": positive,
|
||||
"boxTextFgError": negative,
|
||||
# Folder strip in the view color, so the selected folder merges into
|
||||
# the chat list next to it.
|
||||
"sideBarBg": view,
|
||||
"sideBarBgActive": window,
|
||||
"sideBarBgRipple": mix(view, window_text, 0.1),
|
||||
"sideBarTextFg": muted,
|
||||
"sideBarTextFgActive": link,
|
||||
"sideBarIconFg": muted,
|
||||
"sideBarIconFgActive": link,
|
||||
"sideBarBadgeBg": accent,
|
||||
"sideBarBadgeBgMuted": control,
|
||||
"sideBarBadgeFg": accent_text,
|
||||
"dialogsBg": window,
|
||||
"dialogsBgOver": hover,
|
||||
"dialogsRippleBg": ripple,
|
||||
"dialogsBgActive": accent,
|
||||
"dialogsRippleBgActive": accent_hover,
|
||||
"dialogsNameFg": window_text,
|
||||
"dialogsTextFg": muted,
|
||||
"dialogsTextFgOver": muted,
|
||||
"dialogsTextFgService": link,
|
||||
"dialogsTextFgServiceOver": link,
|
||||
"dialogsDateFg": muted,
|
||||
"dialogsDateFgOver": muted,
|
||||
"dialogsDraftFg": negative,
|
||||
"dialogsOnlineBadgeFg": positive,
|
||||
"dialogsVerifiedIconBg": link,
|
||||
"dialogsVerifiedIconFg": window,
|
||||
"dialogsUnreadBg": accent,
|
||||
"dialogsUnreadBgOver": accent,
|
||||
"dialogsUnreadFg": accent_text,
|
||||
"dialogsUnreadBgMuted": control,
|
||||
"dialogsUnreadBgMutedOver": mix(window, window_text, 0.35),
|
||||
"dialogsUnreadBgActive": accent_text,
|
||||
"dialogsUnreadFgActive": accent,
|
||||
"dialogsUnreadBgMutedActive": mix(accent, accent_text, 0.55),
|
||||
"historyComposeAreaBg": window,
|
||||
"historyComposeAreaFg": window_text,
|
||||
"historyComposeButtonBgOver": hover,
|
||||
"historyComposeButtonBgRipple": ripple,
|
||||
"historyTextInFg": view_text,
|
||||
"historyTextOutFg": view_text,
|
||||
"historyLinkInFg": link,
|
||||
"msgInBg": msg_in,
|
||||
"msgInBgSelected": mix(msg_in, accent, 0.3),
|
||||
"msgOutBg": msg_out,
|
||||
"msgOutBgSelected": mix(msg_out, accent, 0.3),
|
||||
"msgInDateFg": muted,
|
||||
"msgOutDateFg": mix(msg_out, WHITE, 0.55),
|
||||
"msgSelectOverlay": (*accent, 0x4C),
|
||||
"msgStickerOverlay": (*accent, 0x7F),
|
||||
}
|
||||
|
||||
|
||||
def luma(c):
|
||||
return 0.299 * c[0] + 0.587 * c[1] + 0.114 * c[2]
|
||||
|
||||
|
||||
def interp(points, x):
|
||||
for (x0, y0), (x1, y1) in zip(points, points[1:]):
|
||||
if x <= x1:
|
||||
return y0 + (y1 - y0) * (x - x0) / (x1 - x0)
|
||||
return points[-1][1]
|
||||
|
||||
|
||||
def parse_hex(v):
|
||||
h = v[1:]
|
||||
if len(h) in (3, 4):
|
||||
h = "".join(ch * 2 for ch in h)
|
||||
return tuple(int(h[i:i + 2], 16) for i in range(0, len(h), 2))
|
||||
|
||||
|
||||
def telegram_theme(color):
|
||||
"""The night theme, recolored onto the scheme.
|
||||
|
||||
Night tints everything blue. Its tinted grays become scheme grays of
|
||||
matching brightness, its blues take the accent's hue and saturation, and
|
||||
other hues (reds, greens, name colors) stay. telegram() then pins every
|
||||
key that has a scheme role.
|
||||
"""
|
||||
base = TELEGRAM_BASE.read_text()
|
||||
line_re = re.compile(r"^(\w+):\s*(#[0-9a-fA-F]+)\s*;")
|
||||
night = {m[1]: parse_hex(m[2]) for m in map(line_re.match, base.splitlines()) if m}
|
||||
|
||||
window, muted = color("Window", "BackgroundNormal"), color("Window", "ForegroundInactive")
|
||||
grays = sorted({
|
||||
(0, 0),
|
||||
(luma(night["windowBg"]), window[0]),
|
||||
(luma(night["windowSubTextFg"]), muted[0]),
|
||||
(255, 255),
|
||||
})
|
||||
accent_h, _, accent_s = colorsys.rgb_to_hls(*(v / 255 for v in color("Selection", "BackgroundNormal")))
|
||||
|
||||
def recolor(c):
|
||||
rgb, alpha = c[:3], c[3:]
|
||||
chroma = max(rgb) - min(rgb)
|
||||
h, l, _ = colorsys.rgb_to_hls(*(v / 255 for v in rgb))
|
||||
if chroma <= 4 or not 195 <= h * 360 <= 225:
|
||||
return c
|
||||
if chroma <= 55:
|
||||
g = round(interp(grays, luma(rgb)))
|
||||
return (g, g, g, *alpha)
|
||||
return (*(round(v * 255) for v in colorsys.hls_to_rgb(accent_h, l, accent_s)), *alpha)
|
||||
|
||||
pinned = telegram(color)
|
||||
seen = set()
|
||||
out = [f"// Generated by darwin-breeze/build.py from {SCHEME.name}"]
|
||||
for line in base.splitlines():
|
||||
m = line_re.match(line)
|
||||
if m:
|
||||
key = m[1]
|
||||
seen.add(key)
|
||||
c = pinned[key] if key in pinned else recolor(night[key])
|
||||
line = f"{key}: {hexc(c)};" + line[m.end():]
|
||||
elif re.match(r"^(\w+):", line) and line.split(":")[0] in pinned:
|
||||
key = line.split(":")[0]
|
||||
seen.add(key)
|
||||
line = f"{key}: {hexc(pinned[key])};"
|
||||
out.append(line)
|
||||
out += [f"{k}: {hexc(c)};" for k, c in pinned.items() if k not in seen]
|
||||
return "\n".join(out) + "\n"
|
||||
|
||||
|
||||
def vscode_base_colors():
|
||||
"""Dark 2026's colors with its include chain resolved."""
|
||||
chain, path = [], VSCODE_UI
|
||||
while path:
|
||||
theme = json.loads(path.read_text())
|
||||
chain.append(theme.get("colors", {}))
|
||||
path = path.parent / theme["include"] if "include" in theme else None
|
||||
colors = {}
|
||||
for layer in reversed(chain):
|
||||
colors.update(layer)
|
||||
return {k: parse_hex(v) for k, v in colors.items()}
|
||||
|
||||
|
||||
def vscode(color):
|
||||
"""Workbench colors: Dark 2026, recolored onto the scheme.
|
||||
|
||||
Dark 2026 lays out its grays the way the scheme does: the chrome lighter
|
||||
than the editor, widgets lighter still. Each of those grays becomes the
|
||||
scheme gray in the same place, and the grays in between are interpolated.
|
||||
Each hue takes the hue of the matching scheme color but keeps its own
|
||||
lightness and saturation, so a muted dark red background stays one. pins
|
||||
then sets the keys that have an exact scheme role, which is where the full
|
||||
macOS blue shows.
|
||||
"""
|
||||
window, window_text = color("Window", "BackgroundNormal"), color("Window", "ForegroundNormal")
|
||||
view, view_text = color("View", "BackgroundNormal"), color("View", "ForegroundNormal")
|
||||
accent, accent_text = color("Selection", "BackgroundNormal"), color("Selection", "ForegroundNormal")
|
||||
muted = color("Window", "ForegroundInactive")
|
||||
negative = color("View", "ForegroundNegative")
|
||||
neutral = color("View", "ForegroundNeutral")
|
||||
positive = color("View", "ForegroundPositive")
|
||||
link = color("View", "ForegroundLink")
|
||||
base = vscode_base_colors()
|
||||
|
||||
grays = sorted({
|
||||
(0, 0),
|
||||
(luma(base["editor.background"]), view[0]),
|
||||
(luma(base["sideBar.background"]), window[0]),
|
||||
(luma(base["editorWidget.background"]), color("Window", "BackgroundAlternate")[0]),
|
||||
(luma(base["sideBar.border"]), mix(window, window_text, 0.115)[0]),
|
||||
(luma(base["descriptionForeground"]), muted[0]),
|
||||
(luma(base["foreground"]), window_text[0]),
|
||||
(255, 255),
|
||||
})
|
||||
assert all(a[1] <= b[1] for a, b in zip(grays, grays[1:])), grays
|
||||
|
||||
def hls(c):
|
||||
return colorsys.rgb_to_hls(*(v / 255 for v in c))
|
||||
|
||||
# (hue range in degrees, scheme color whose hue it takes)
|
||||
families = [
|
||||
((0, 15), negative),
|
||||
((15, 70), neutral),
|
||||
((70, 170), positive),
|
||||
((170, 250), accent),
|
||||
((250, 330), color("View", "ForegroundVisited")),
|
||||
((330, 360), negative),
|
||||
]
|
||||
|
||||
def recolor(c):
|
||||
rgb, alpha = c[:3], c[3:]
|
||||
if max(rgb) - min(rgb) <= 10:
|
||||
g = round(interp(grays, luma(rgb)))
|
||||
return (g, g, g, *alpha)
|
||||
h, l, sat = hls(rgb)
|
||||
target = next(t for (lo, hi), t in families if lo <= h * 360 < hi)
|
||||
return (*(round(v * 255) for v in colorsys.hls_to_rgb(hls(target)[0], l, sat)), *alpha)
|
||||
|
||||
pins = {
|
||||
"editor.background": view,
|
||||
"editor.foreground": view_text,
|
||||
"titleBar.inactiveBackground": color("Header][Inactive", "BackgroundNormal"),
|
||||
"titleBar.inactiveForeground": color("Header][Inactive", "ForegroundNormal"),
|
||||
"focusBorder": (*accent, 0xB3),
|
||||
"quickInputList.focusBackground": accent,
|
||||
"button.background": accent,
|
||||
"button.border": accent,
|
||||
"button.hoverBackground": color("Selection", "BackgroundAlternate"),
|
||||
"button.foreground": accent_text,
|
||||
"badge.background": accent,
|
||||
"badge.foreground": accent_text,
|
||||
"activityBarBadge.background": accent,
|
||||
"activityBarBadge.foreground": accent_text,
|
||||
"tab.activeBorderTop": accent,
|
||||
"panelTitle.activeBorder": accent,
|
||||
"textLink.foreground": link,
|
||||
"textLink.activeForeground": link,
|
||||
}
|
||||
names = ["Black", "Red", "Green", "Yellow", "Blue", "Magenta", "Cyan", "White"]
|
||||
for i, c in enumerate(ansi(color)):
|
||||
pins[f"terminal.ansi{'Bright' if i >= 8 else ''}{names[i % 8]}"] = c
|
||||
|
||||
colors = {k: pins.get(k) or recolor(c) for k, c in base.items()}
|
||||
colors.update(pins)
|
||||
# Electron is Chromium, so it needs the same KWin compensation as Helium.
|
||||
return {k: hexc(kwin_compensate(c[:3]) + list(c[3:])) for k, c in sorted(colors.items())}
|
||||
|
||||
|
||||
def vscode_files(color):
|
||||
"""The extension files, keyed by their path inside it."""
|
||||
name = "Darwin Breeze Dark"
|
||||
# Syntax colors as T3 Code shows them. T3 Code doesn't compensate them
|
||||
# either, so the two render the same.
|
||||
syntax = json.loads(VSCODE_SYNTAX.read_text())
|
||||
theme = {
|
||||
"$schema": "vscode://schemas/color-theme",
|
||||
"name": name,
|
||||
"type": "dark",
|
||||
"semanticHighlighting": True,
|
||||
"colors": vscode(color),
|
||||
"tokenColors": syntax["tokenColors"],
|
||||
"semanticTokenColors": syntax["semanticTokenColors"],
|
||||
}
|
||||
package = {
|
||||
"name": "darwin-breeze",
|
||||
"displayName": "Darwin Breeze",
|
||||
"description": f"Generated by darwin-breeze/build.py from {SCHEME.name}",
|
||||
"publisher": "trivk",
|
||||
"version": "1.0.0",
|
||||
"engines": {"vscode": "^1.70.0"},
|
||||
"categories": ["Themes"],
|
||||
"contributes": {
|
||||
"themes": [{"label": name, "uiTheme": "vs-dark", "path": f"./{VSCODE_THEME}"}],
|
||||
},
|
||||
}
|
||||
return {
|
||||
"package.json": json.dumps(package, indent=2) + "\n",
|
||||
VSCODE_THEME: json.dumps(theme, indent=2) + "\n",
|
||||
}
|
||||
|
||||
|
||||
def vsix(files):
|
||||
"""Pack the extension the way vsce does, for code --install-extension."""
|
||||
manifest = """<?xml version="1.0" encoding="utf-8"?>
|
||||
<PackageManifest Version="2.0.0" xmlns="http://schemas.microsoft.com/developer/vsx-schema/2011" xmlns:d="http://schemas.microsoft.com/developer/vsx-schema-design/2011">
|
||||
<Metadata>
|
||||
<Identity Language="en-US" Id="darwin-breeze" Version="1.0.0" Publisher="trivk"/>
|
||||
<DisplayName>Darwin Breeze</DisplayName>
|
||||
<Description xml:space="preserve">Darwin Breeze Dark color theme</Description>
|
||||
<Categories>Themes</Categories>
|
||||
<Properties>
|
||||
<Property Id="Microsoft.VisualStudio.Code.Engine" Value="^1.70.0"/>
|
||||
</Properties>
|
||||
</Metadata>
|
||||
<Installation>
|
||||
<InstallationTarget Id="Microsoft.VisualStudio.Code"/>
|
||||
</Installation>
|
||||
<Dependencies/>
|
||||
<Assets>
|
||||
<Asset Type="Microsoft.VisualStudio.Code.Manifest" Path="extension/package.json" Addressable="true"/>
|
||||
</Assets>
|
||||
</PackageManifest>
|
||||
"""
|
||||
content_types = """<?xml version="1.0" encoding="utf-8"?>
|
||||
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension=".json" ContentType="application/json"/><Default Extension=".vsixmanifest" ContentType="text/xml"/></Types>
|
||||
"""
|
||||
return zip_bytes({
|
||||
"[Content_Types].xml": content_types,
|
||||
"extension.vsixmanifest": manifest,
|
||||
**{f"extension/{path}": text for path, text in files.items()},
|
||||
})
|
||||
|
||||
|
||||
def solid_png(c):
|
||||
"""A 1x1 PNG; Telegram stretches it over the chat as a plain background."""
|
||||
|
||||
def chunk(kind, data):
|
||||
return struct.pack(">I", len(data)) + kind + data + struct.pack(">I", zlib.crc32(kind + data))
|
||||
|
||||
return (
|
||||
b"\x89PNG\r\n\x1a\n"
|
||||
+ chunk(b"IHDR", struct.pack(">IIBBBBB", 1, 1, 8, 2, 0, 0, 0))
|
||||
+ chunk(b"IDAT", zlib.compress(bytes([0, *c])))
|
||||
+ chunk(b"IEND", b"")
|
||||
)
|
||||
|
||||
|
||||
def zip_bytes(files):
|
||||
"""A zip with fixed timestamps, so an unchanged theme rebuilds byte-identical."""
|
||||
buf = io.BytesIO()
|
||||
with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as z:
|
||||
for name, data in files.items():
|
||||
info = zipfile.ZipInfo(name, (1980, 1, 1, 0, 0, 0))
|
||||
info.compress_type = zipfile.ZIP_DEFLATED
|
||||
info.external_attr = 0o644 << 16
|
||||
z.writestr(info, data)
|
||||
return buf.getvalue()
|
||||
|
||||
|
||||
def write(path, data):
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
if path.exists() and path.read_text() == text:
|
||||
if isinstance(data, str):
|
||||
data = data.encode()
|
||||
if path.exists() and path.read_bytes() == data:
|
||||
return False
|
||||
path.write_text(text)
|
||||
path.write_bytes(data)
|
||||
print(f"wrote {path.relative_to(PKG)}")
|
||||
return True
|
||||
|
||||
@@ -245,6 +667,16 @@ def main():
|
||||
theme = {"version": 1, "name": "Darwin Breeze", "appearance": "dark", "colors": t3(color)}
|
||||
write(T3, json.dumps(theme, indent=2) + "\n")
|
||||
|
||||
write(TELEGRAM, zip_bytes({
|
||||
"colors.tdesktop-theme": telegram_theme(color),
|
||||
"background.png": solid_png(color("View", "BackgroundNormal")),
|
||||
}))
|
||||
|
||||
files = vscode_files(color)
|
||||
for path, text in files.items():
|
||||
write(VSCODE / path, text)
|
||||
write(VSCODE / "darwin-breeze.vsix", vsix(files))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in new issue
Block a user