Files
NetMesh/scripts/generate-app-icon-variants.py
zhaolei 3c72efcb7f
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
[Init] Initial commit - NetMesh terminal manager
2026-09-13 18:24:01 +08:00

247 lines
6.9 KiB
Python

#!/usr/bin/env python3
"""Generate runtime app icon variants from public/icon.svg.
Outputs desktop PNGs under public/icons/variants/ and HIG-sized macOS PNGs
under public/icons/variants/macos/ for Electron dock/taskbar switching.
Run: python3 scripts/generate-app-icon-variants.py
Requires: rsvg-convert (librsvg)
"""
from __future__ import annotations
import re
import subprocess
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
SOURCE_SVG = ROOT / "public" / "icon.svg"
OUT_DIR = ROOT / "public" / "icons" / "variants"
MACOS_OUT_DIR = OUT_DIR / "macos"
MACOS_RUNTIME_VIEWBOX = "0 0 1024 1024"
DETAIL_COLORS = [
"#1f2657",
"#18214c",
"#0c1943",
"#505c83",
"#919ab0",
"#022551",
"#032551",
"#0c1a4d",
"#98a2bf",
"#9ea6be",
"#132152",
"#6c7794",
"#6f7b97",
"#a8aec5",
"#677393",
"#01103f",
"#7581a0",
"#a7aec3",
"#adb2c9",
"#bec4d7",
"#9ba0b8",
"#9aa5bc",
"#adb1c6",
"#c9d0dc",
"#b5bfcb",
"#8193aa",
]
RAINBOW_GRADIENT = """
<linearGradient id="netcatty-rainbow" x1="180" y1="1020" x2="1080" y2="260" gradientUnits="userSpaceOnUse">
<stop offset="0%" stop-color="#EF4444"/>
<stop offset="16%" stop-color="#F97316"/>
<stop offset="33%" stop-color="#EAB308"/>
<stop offset="50%" stop-color="#22C55E"/>
<stop offset="66%" stop-color="#06B6D4"/>
<stop offset="83%" stop-color="#3B82F6"/>
<stop offset="100%" stop-color="#A855F7"/>
</linearGradient>
"""
WHITE_BG = {
"bg": "#FFFFFF",
"border_color": "#CBD5E1",
"border_opacity": "0.85",
}
WHITE_CAT_FACE = "#FFFFFF"
VARIANTS: dict[str, dict[str, str] | None] = {
"original": None,
"bright": {
"bg": "#0EA5E9",
"cat": "#FFFFFF",
"cat_alt": "#F0F9FF",
"detail": "#0369A1",
"border_color": "#ffffff",
"border_opacity": "0.55",
},
"dark": {
"bg": "#0F172A",
"cat": "#F8FAFC",
"cat_alt": "#E2E8F0",
"detail": "#334155",
"border_color": "#ffffff",
"border_opacity": "0.35",
},
"colorful": {
"bg": "#EA580C",
"cat": "#FFFFFF",
"cat_alt": "#FFF7ED",
"detail": "#C2410C",
"border_color": "#ffffff",
"border_opacity": "0.5",
},
"high-contrast": {
"bg": "#000000",
"cat": "#FACC15",
"cat_alt": "#FDE047",
"detail": "#A16207",
"border_color": "#ffffff",
"border_opacity": "0.9",
},
"white-navy": {
**WHITE_BG,
"cat": "#002551",
"cat_alt": "#0B3D78",
"detail": WHITE_CAT_FACE,
},
"white-sky": {
**WHITE_BG,
"cat": "#0284C7",
"cat_alt": "#38BDF8",
"detail": WHITE_CAT_FACE,
},
"white-rose": {
**WHITE_BG,
"cat": "#E11D48",
"cat_alt": "#FB7185",
"detail": WHITE_CAT_FACE,
},
"white-emerald": {
**WHITE_BG,
"cat": "#059669",
"cat_alt": "#34D399",
"detail": WHITE_CAT_FACE,
},
"white-amber": {
**WHITE_BG,
"cat": "#D97706",
"cat_alt": "#FBBF24",
"detail": WHITE_CAT_FACE,
},
"white-violet": {
**WHITE_BG,
"cat": "#7C3AED",
"cat_alt": "#A78BFA",
"detail": WHITE_CAT_FACE,
},
"rainbow": {
**WHITE_BG,
"mode": "rainbow",
"detail": WHITE_CAT_FACE,
},
}
def load_template() -> str:
if not SOURCE_SVG.exists():
raise SystemExit(f"source svg not found: {SOURCE_SVG}")
return SOURCE_SVG.read_text(encoding="utf-8")
def set_viewbox(svg: str, viewbox: str) -> str:
out, count = re.subn(r'viewBox="[^"]+"', f'viewBox="{viewbox}"', svg, count=1)
if count != 1:
raise SystemExit("source svg is missing a root viewBox")
return out
def inject_rainbow_gradient(svg: str) -> str:
if "id=\"netcatty-rainbow\"" in svg:
return svg
return svg.replace("<defs>", f"<defs>{RAINBOW_GRADIENT}", 1)
def apply_solid_variant(svg: str, spec: dict[str, str]) -> str:
out = svg
out = out.replace('fill="#002551"', f'fill="{spec["bg"]}"', 1)
out = out.replace('fill="#f9f9f9"', f'fill="{spec["cat"]}"')
out = out.replace('fill="#f8f8f9"', f'fill="{spec["cat_alt"]}"')
for color in DETAIL_COLORS:
out = out.replace(f'fill="{color}"', f'fill="{spec["detail"]}"')
border_color = spec.get("border_color", "#ffffff")
border_opacity = spec.get("border_opacity", "0.4")
out = re.sub(
r'stroke="#ffffff" stroke-opacity="[^"]+"',
f'stroke="{border_color}" stroke-opacity="{border_opacity}"',
out,
count=1,
)
return out
def apply_rainbow_variant(svg: str, spec: dict[str, str]) -> str:
out = inject_rainbow_gradient(svg)
out = out.replace('fill="#002551"', f'fill="{spec["bg"]}"', 1)
rainbow_fill = 'fill="url(#netcatty-rainbow)"'
out = out.replace('fill="#f9f9f9"', rainbow_fill)
out = out.replace('fill="#f8f8f9"', rainbow_fill)
face_color = spec.get("detail", WHITE_CAT_FACE)
for color in DETAIL_COLORS:
out = out.replace(f'fill="{color}"', f'fill="{face_color}"')
border_color = spec.get("border_color", "#CBD5E1")
border_opacity = spec.get("border_opacity", "0.85")
out = re.sub(
r'stroke="#ffffff" stroke-opacity="[^"]+"',
f'stroke="{border_color}" stroke-opacity="{border_opacity}"',
out,
count=1,
)
return out
def apply_variant(svg: str, spec: dict[str, str]) -> str:
if spec.get("mode") == "rainbow":
return apply_rainbow_variant(svg, spec)
return apply_solid_variant(svg, spec)
def render_png(svg_content: str, target: Path) -> None:
target.parent.mkdir(parents=True, exist_ok=True)
subprocess.run(
["rsvg-convert", "-w", "1024", "-h", "1024", "-o", str(target)],
input=svg_content.encode("utf-8"),
check=True,
)
def main() -> None:
desktop_template = load_template()
macos_template = set_viewbox(desktop_template, MACOS_RUNTIME_VIEWBOX)
OUT_DIR.mkdir(parents=True, exist_ok=True)
MACOS_OUT_DIR.mkdir(parents=True, exist_ok=True)
for variant_id, spec in VARIANTS.items():
if spec is None:
desktop_path = OUT_DIR / f"{variant_id}.png"
render_png(desktop_template, desktop_path)
print(f"wrote {desktop_path.relative_to(ROOT)}")
macos_path = MACOS_OUT_DIR / f"{variant_id}.png"
render_png(macos_template, macos_path)
print(f"wrote {macos_path.relative_to(ROOT)}")
continue
desktop_path = OUT_DIR / f"{variant_id}.png"
render_png(apply_variant(desktop_template, spec), desktop_path)
print(f"wrote {desktop_path.relative_to(ROOT)}")
macos_path = MACOS_OUT_DIR / f"{variant_id}.png"
render_png(apply_variant(macos_template, spec), macos_path)
print(f"wrote {macos_path.relative_to(ROOT)}")
if __name__ == "__main__":
main()