#!/usr/bin/env python3 """ NexusOS folder icon generator. Composes each variant SVG from a shared master + a per-variant badge fragment, then rasterizes each to PNG at 16/22/24/32/48/64/128 and drops the result into ~/.icons/NexusOS//places/.png. Run from this directory: python3 build_icons.py [--svg-only] [--no-cache-update] """ from __future__ import annotations import argparse import base64 import pathlib import shutil import subprocess import sys from textwrap import dedent ROOT = pathlib.Path(__file__).resolve().parent PLACES_DIR = ROOT / "places" LOGO_SRC = ROOT.parent.parent / "assets" / "n-small.png" ICONS_OUT = pathlib.Path.home() / ".icons" / "NexusOS" SIZES = (16, 22, 24, 32, 48, 64, 128) # ── colors (mirror gtk-3.0/colors.css) ─────────────────────────────────────── BODY = "#5e0066" # brand_purple_dark — folder body HIGHLIGHT = "#88008f" # brand_purple — top-of-face highlight stripe TAB = "#8cc63f" # brand_green — folder tab TAB_BRIGHT = "#b8e373" # brand_green_light — drag-accept tab LIP = "#f2f2f2" # text_primary — inner lip stripe & badge fill # Embedded logo (base64) populated at runtime _LOGO_B64: str | None = None def logo_data_uri() -> str: global _LOGO_B64 if _LOGO_B64 is None: _LOGO_B64 = base64.b64encode(LOGO_SRC.read_bytes()).decode("ascii") return f"data:image/png;base64,{_LOGO_B64}" def folder_svg(badge: str, *, tab_color: str = TAB) -> str: """Build a full variant SVG by injecting `badge` into the master template.""" return dedent(f"""\ {badge} """) # ── badge fragments (centered on front face ~x=64, y=78) ───────────────────── # Each is a simple white-on-purple glyph kept readable at 16-22px. BADGES: dict[str, str] = { # Base folder — no badge "folder": "", # Folder-open: lighter tab + slight perspective on front (simple variant) "folder-open": ( f'' ), # Documents — page with folded corner "folder-documents": ( f'' f'' ), # Download — down arrow "folder-download": ( f'' ), # Drag-accept — uses bright green tab, plus white plus glyph "folder-drag-accept": ( f'' ), # Home — house "folder-home": ( f'' ), # Music — eighth note "folder-music": ( f'' f'' f'' ), # Pictures — sun over mountains "folder-pictures": ( f'' f'' f'' ), # Publicshare — two people "folder-publicshare": ( f'' f'' f'' ), # Recent — clock face "folder-recent": ( f'' f'' ), # Remote — globe (circle + ellipse + meridian) "folder-remote": ( f'' f'' f'' ), # Saved search — magnifying glass "folder-saved-search": ( f'' f'' ), # Templates — page with horizontal lines "folder-templates": ( f'' f'' f'' f'' f'' f'' ), # Videos — play triangle "folder-videos": ( f'' ), } def nexus_core_svg() -> str: """folder-nexus-core embeds the bicolor N logo via base64 data URI.""" return dedent(f"""\ """) def write_variants() -> list[tuple[str, pathlib.Path]]: PLACES_DIR.mkdir(parents=True, exist_ok=True) written: list[tuple[str, pathlib.Path]] = [] for name, badge in BADGES.items(): tab = TAB_BRIGHT if name == "folder-drag-accept" else TAB path = PLACES_DIR / f"{name}.svg" path.write_text(folder_svg(badge, tab_color=tab)) written.append((name, path)) nx_path = PLACES_DIR / "folder-nexus-core.svg" nx_path.write_text(nexus_core_svg()) written.append(("folder-nexus-core", nx_path)) return written def render_one(svg_path: pathlib.Path, name: str) -> None: for size in SIZES: out_dir = ICONS_OUT / f"{size}x{size}" / "places" out_dir.mkdir(parents=True, exist_ok=True) out_png = out_dir / f"{name}.png" subprocess.run( [ "inkscape", str(svg_path), "--export-type=png", f"--export-filename={out_png}", f"--export-width={size}", f"--export-height={size}", ], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, ) print(f" {name} → {size}x{size}") def backup_existing() -> None: backup = ROOT / "_backup" / "folder-nexus-core-pre-rebuild" if backup.exists(): return backup.mkdir(parents=True, exist_ok=True) for size in SIZES: src = ICONS_OUT / f"{size}x{size}" / "places" / "folder-nexus-core.png" if src.exists(): shutil.copy2(src, backup / f"{size}-folder-nexus-core.png") print(f"Backed up existing folder-nexus-core PNGs → {backup}") def main() -> int: parser = argparse.ArgumentParser() parser.add_argument("--svg-only", action="store_true", help="only write SVG sources, skip rasterization") parser.add_argument("--no-cache-update", action="store_true", help="skip gtk-update-icon-cache at the end") args = parser.parse_args() if not LOGO_SRC.exists(): print(f"ERROR: logo source missing at {LOGO_SRC}", file=sys.stderr) return 1 print("Writing variant SVGs…") variants = write_variants() print(f" {len(variants)} SVGs written to {PLACES_DIR}") if args.svg_only: return 0 backup_existing() print("Rendering PNGs…") for name, svg_path in variants: render_one(svg_path, name) if not args.no_cache_update: print("Refreshing icon cache…") subprocess.run( ["gtk-update-icon-cache", "-f", "-t", str(ICONS_OUT)], check=False, ) print(f"\nDone. Rendered {len(variants)} variants × {len(SIZES)} sizes " f"= {len(variants) * len(SIZES)} PNGs.") return 0 if __name__ == "__main__": raise SystemExit(main())