Build & Release / Lint & Typecheck (push) Successful in 36s
Build & Release / Unit Tests (push) Successful in 34s
Build & Release / Windows (x64) (push) Failing after 34s
Build & Release / Windows (arm64) (push) Failing after 34s
Build & Release / macOS (x64) (push) Canceled after 0s
Build & Release / macOS (arm64) (push) Canceled after 0s
Build & Release / Linux (arm64) (push) Canceled after 50s
Build & Release / Linux (x64) (push) Canceled after 0s
Build & Release / Publish Release (push) Canceled after 0s
Replace all orange/red icon assets with new brand colors: - Circle background #191919, logo mark #FBFAF5 - Regenerated: icon.png, icon.ico, icon.icns, all tray icons - Updated generate-icons.py with new palette and circle shape - Added source logo.svg - Bump version to 0.4.92 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
224 lines
6.7 KiB
Python
224 lines
6.7 KiB
Python
#!/usr/bin/env python3
|
|
"""Generate AgentCompany app icons (PNG, ICO, ICNS) and tray icons.
|
|
|
|
Design v4: Circle background #191919 with #FBFAF5 "A" logo.
|
|
Uses the actual logo paths from logo.svg rendered via cairosvg/Pillow.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import shutil
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
from PIL import Image, ImageDraw
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
ASSETS = ROOT / "assets"
|
|
ICONSET = ASSETS / "icon.iconset"
|
|
PNG_PATH = ASSETS / "icon.png"
|
|
ICO_PATH = ASSETS / "icon.ico"
|
|
ICNS_PATH = ASSETS / "icon.icns"
|
|
SVG_PATH = ASSETS / "icon.svg"
|
|
SIZE = 1024
|
|
|
|
# ── Brand palette ──────────────────────────────────────────────────────
|
|
BG_COLOR = (25, 25, 25) # #191919
|
|
LOGO_COLOR = (251, 250, 245) # #FBFAF5
|
|
|
|
|
|
def build_icon() -> Image.Image:
|
|
"""Build the app icon by rendering icon.svg via cairosvg, or fallback to Pillow."""
|
|
# Try cairosvg first (best quality)
|
|
try:
|
|
import cairosvg
|
|
svg_data = SVG_PATH.read_bytes()
|
|
png_data = cairosvg.svg2png(bytestring=svg_data, output_width=SIZE, output_height=SIZE)
|
|
import io
|
|
return Image.open(io.BytesIO(png_data)).convert("RGBA")
|
|
except Exception:
|
|
pass
|
|
|
|
# Fallback: draw programmatically with Pillow
|
|
icon = Image.new("RGBA", (SIZE, SIZE), (0, 0, 0, 0))
|
|
draw = ImageDraw.Draw(icon)
|
|
|
|
# Circle background
|
|
margin = SIZE // 2 - 480
|
|
draw.ellipse(
|
|
(margin, margin, SIZE - margin, SIZE - margin),
|
|
fill=(*BG_COLOR, 255),
|
|
)
|
|
|
|
# Draw the "A" monogram — same geometry as the old icon but in new color
|
|
cx = SIZE // 2
|
|
apex_y = 200
|
|
base_y = 800
|
|
leg_w = 64
|
|
base_half = 250
|
|
fill = (*LOGO_COLOR, 245)
|
|
|
|
def leg_x(y: int) -> tuple[float, float]:
|
|
t = (y - apex_y) / (base_y - apex_y)
|
|
spread = base_half * t
|
|
return (cx - spread, cx + spread)
|
|
|
|
ll_top, _ = leg_x(apex_y)
|
|
ll_bot, _ = leg_x(base_y)
|
|
_, lr_top = leg_x(apex_y)
|
|
_, lr_bot = leg_x(base_y)
|
|
|
|
left_leg = [
|
|
(ll_top - leg_w * 0.35, apex_y),
|
|
(ll_top + leg_w * 0.35, apex_y),
|
|
(ll_bot + leg_w, base_y),
|
|
(ll_bot - leg_w, base_y),
|
|
]
|
|
right_leg = [
|
|
(lr_top - leg_w * 0.35, apex_y),
|
|
(lr_top + leg_w * 0.35, apex_y),
|
|
(lr_bot + leg_w, base_y),
|
|
(lr_bot - leg_w, base_y),
|
|
]
|
|
|
|
draw.polygon(left_leg, fill=fill)
|
|
draw.polygon(right_leg, fill=fill)
|
|
|
|
# Crossbars — all same cream color with varying opacity for hierarchy
|
|
bar_h = 42
|
|
bar_r = 12
|
|
bar_pad = 14
|
|
|
|
tiers = [
|
|
(440, (*LOGO_COLOR, 255)),
|
|
(545, (*LOGO_COLOR, 210)),
|
|
(650, (*LOGO_COLOR, 175)),
|
|
]
|
|
|
|
for bar_cy, bar_fill in tiers:
|
|
left_cx, right_cx = leg_x(bar_cy)
|
|
bx0 = int(left_cx + bar_pad)
|
|
bx1 = int(right_cx - bar_pad)
|
|
draw.rounded_rectangle(
|
|
(bx0, bar_cy - bar_h // 2, bx1, bar_cy + bar_h // 2),
|
|
radius=bar_r,
|
|
fill=bar_fill,
|
|
)
|
|
|
|
# Apex beacon
|
|
dot_r = 20
|
|
dot_cy = apex_y - 2
|
|
draw.ellipse(
|
|
(cx - dot_r, dot_cy - dot_r, cx + dot_r, dot_cy + dot_r),
|
|
fill=(*LOGO_COLOR, 255),
|
|
)
|
|
|
|
return icon
|
|
|
|
|
|
def build_tray_icons() -> None:
|
|
"""Generate tray/notification area icons for all platforms."""
|
|
# Color tray icon (Windows/Linux) — 32x32 and 64x64
|
|
for name, size in [("tray-icon.png", 32), ("tray-icon@2x.png", 64)]:
|
|
img = Image.new("RGBA", (size, size), (0, 0, 0, 0))
|
|
draw = ImageDraw.Draw(img)
|
|
margin = 1
|
|
draw.ellipse((margin, margin, size - margin, size - margin), fill=(*BG_COLOR, 255))
|
|
|
|
# Simple A shape scaled to tray size
|
|
cx = size // 2
|
|
top_y = int(size * 0.18)
|
|
bot_y = int(size * 0.82)
|
|
spread = int(size * 0.28)
|
|
w = max(2, int(size * 0.06))
|
|
|
|
draw.polygon([
|
|
(cx - w, top_y), (cx + w, top_y),
|
|
(cx + spread + w, bot_y), (cx + spread - w, bot_y),
|
|
], fill=(*LOGO_COLOR, 240))
|
|
draw.polygon([
|
|
(cx - w, top_y), (cx + w, top_y),
|
|
(cx - spread + w, bot_y), (cx - spread - w, bot_y),
|
|
], fill=(*LOGO_COLOR, 240))
|
|
|
|
# Crossbar
|
|
bar_y = int(size * 0.52)
|
|
bar_h = max(2, int(size * 0.06))
|
|
bar_left = cx - int(spread * 0.55)
|
|
bar_right = cx + int(spread * 0.55)
|
|
draw.rectangle((bar_left, bar_y, bar_right, bar_y + bar_h), fill=(*LOGO_COLOR, 255))
|
|
|
|
img.save(ASSETS / name, format="PNG")
|
|
|
|
# macOS template icons (monochrome black + alpha)
|
|
for name, size in [("tray-iconTemplate.png", 22), ("tray-iconTemplate@2x.png", 44)]:
|
|
img = Image.new("RGBA", (size, size), (0, 0, 0, 0))
|
|
draw = ImageDraw.Draw(img)
|
|
|
|
cx = size // 2
|
|
top_y = int(size * 0.15)
|
|
bot_y = int(size * 0.85)
|
|
spread = int(size * 0.3)
|
|
w = max(1, int(size * 0.06))
|
|
|
|
draw.polygon([
|
|
(cx - w, top_y), (cx + w, top_y),
|
|
(cx + spread + w, bot_y), (cx + spread - w, bot_y),
|
|
], fill=(0, 0, 0, 220))
|
|
draw.polygon([
|
|
(cx - w, top_y), (cx + w, top_y),
|
|
(cx - spread + w, bot_y), (cx - spread - w, bot_y),
|
|
], fill=(0, 0, 0, 220))
|
|
|
|
bar_y = int(size * 0.52)
|
|
bar_h = max(1, int(size * 0.06))
|
|
bar_left = cx - int(spread * 0.55)
|
|
bar_right = cx + int(spread * 0.55)
|
|
draw.rectangle((bar_left, bar_y, bar_right, bar_y + bar_h), fill=(0, 0, 0, 240))
|
|
|
|
img.save(ASSETS / name, format="PNG")
|
|
|
|
|
|
def save_png_and_ico(image: Image.Image) -> None:
|
|
ASSETS.mkdir(parents=True, exist_ok=True)
|
|
image.save(PNG_PATH, format="PNG")
|
|
image.save(
|
|
ICO_PATH,
|
|
format="ICO",
|
|
sizes=[(16, 16), (24, 24), (32, 32), (48, 48), (64, 64), (128, 128), (256, 256)],
|
|
)
|
|
|
|
|
|
def save_icns(image: Image.Image) -> None:
|
|
if ICONSET.exists():
|
|
shutil.rmtree(ICONSET)
|
|
ICONSET.mkdir(parents=True, exist_ok=True)
|
|
|
|
for size in (16, 32, 64, 128, 256, 512):
|
|
image.resize((size, size), Image.Resampling.LANCZOS).save(
|
|
ICONSET / f"icon_{size}x{size}.png", format="PNG",
|
|
)
|
|
image.resize((size * 2, size * 2), Image.Resampling.LANCZOS).save(
|
|
ICONSET / f"icon_{size}x{size}@2x.png", format="PNG",
|
|
)
|
|
|
|
subprocess.run(
|
|
["iconutil", "-c", "icns", str(ICONSET), "-o", str(ICNS_PATH)],
|
|
check=True,
|
|
)
|
|
shutil.rmtree(ICONSET)
|
|
|
|
|
|
def main() -> None:
|
|
ASSETS.mkdir(parents=True, exist_ok=True)
|
|
image = build_icon()
|
|
save_png_and_ico(image)
|
|
save_icns(image)
|
|
build_tray_icons()
|
|
print(f"Generated: {PNG_PATH}, {ICO_PATH}, {ICNS_PATH}")
|
|
print(f"Generated tray icons in {ASSETS}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|