Files
NexusOS/assets/themes/NexusOS-icons-src/build_icons.py
T
jon b0aa0438af Initial commit: NexusOS project + desktop theme baseline
Captures the known-good state after theme consolidation and
consistency reconciliation:
- NexusOS GTK/xfwm4/icon theme assets (assets/themes, management/Mint-Y-Nexus)
- Tightened right-click menus, visible separators, no menu icons
- assets/themes/install-theme.sh: idempotent restore of all wiring
  (symlinks, xfconf xsettings+xfwm4, GTK 3/4 settings.ini)
- .gitignore excludes venv/ollama/models/runtime/db

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 13:39:48 -05:00

271 lines
9.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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/<size>/places/<name>.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"""\
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 128 128">
<path d="M 14 24 Q 14 20 18 20 L 50 20 Q 53 20 55 22 L 62 30 L 110 30
Q 114 30 114 34 L 114 110 Q 114 114 110 114 L 18 114
Q 14 114 14 110 Z" fill="{tab_color}"/>
<rect x="14" y="36" width="100" height="3" fill="{LIP}"/>
<path d="M 14 39 L 114 39 L 114 110 Q 114 114 110 114 L 18 114
Q 14 114 14 110 Z" fill="{BODY}"/>
<rect x="14" y="39" width="100" height="2" fill="{HIGHLIGHT}"/>
<g id="badge">{badge}</g>
</svg>
""")
# ── 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'<path d="M 14 41 L 22 39 L 106 39 L 114 41 L 114 110 '
f'Q 114 114 110 114 L 18 114 Q 14 114 14 110 Z" '
f'fill="{HIGHLIGHT}" opacity="0.55"/>'
),
# Documents — page with folded corner
"folder-documents": (
f'<path d="M 50 58 L 70 58 L 80 68 L 80 100 L 50 100 Z" fill="{LIP}"/>'
f'<path d="M 70 58 L 70 68 L 80 68" fill="none" '
f'stroke="{BODY}" stroke-width="2" stroke-linejoin="miter"/>'
),
# Download — down arrow
"folder-download": (
f'<path d="M 60 58 L 68 58 L 68 80 L 78 80 L 64 98 L 50 80 L 60 80 Z" '
f'fill="{LIP}"/>'
),
# Drag-accept — uses bright green tab, plus white plus glyph
"folder-drag-accept": (
f'<path d="M 60 64 L 68 64 L 68 76 L 80 76 L 80 84 L 68 84 L 68 96 '
f'L 60 96 L 60 84 L 48 84 L 48 76 L 60 76 Z" fill="{LIP}"/>'
),
# Home — house
"folder-home": (
f'<path d="M 64 56 L 84 74 L 80 74 L 80 100 L 70 100 L 70 86 L 58 86 '
f'L 58 100 L 48 100 L 48 74 L 44 74 Z" fill="{LIP}"/>'
),
# Music — eighth note
"folder-music": (
f'<rect x="70" y="56" width="4" height="32" fill="{LIP}"/>'
f'<path d="M 70 56 L 70 64 Q 78 60 82 64 L 82 56 Z" fill="{LIP}"/>'
f'<ellipse cx="64" cy="90" rx="8" ry="6" fill="{LIP}"/>'
),
# Pictures — sun over mountains
"folder-pictures": (
f'<rect x="44" y="60" width="40" height="40" rx="3" fill="{LIP}"/>'
f'<circle cx="74" cy="70" r="3" fill="{BODY}"/>'
f'<path d="M 44 100 L 56 82 L 66 92 L 74 84 L 84 100 Z" fill="{BODY}"/>'
),
# Publicshare — two people
"folder-publicshare": (
f'<circle cx="56" cy="66" r="6" fill="{LIP}"/>'
f'<circle cx="72" cy="66" r="6" fill="{LIP}"/>'
f'<path d="M 46 100 L 46 88 Q 46 78 56 78 L 62 78 Q 64 78 64 80 '
f'Q 64 78 66 78 L 72 78 Q 82 78 82 88 L 82 100 Z" fill="{LIP}"/>'
),
# Recent — clock face
"folder-recent": (
f'<circle cx="64" cy="80" r="14" fill="{LIP}"/>'
f'<path d="M 64 70 L 64 80 L 71 84" fill="none" stroke="{BODY}" '
f'stroke-width="2.5" stroke-linecap="round"/>'
),
# Remote — globe (circle + ellipse + meridian)
"folder-remote": (
f'<circle cx="64" cy="80" r="14" fill="{LIP}"/>'
f'<ellipse cx="64" cy="80" rx="14" ry="6" fill="none" '
f'stroke="{BODY}" stroke-width="2"/>'
f'<line x1="64" y1="66" x2="64" y2="94" stroke="{BODY}" stroke-width="2"/>'
),
# Saved search — magnifying glass
"folder-saved-search": (
f'<circle cx="60" cy="76" r="10" fill="none" stroke="{LIP}" stroke-width="4"/>'
f'<line x1="67" y1="83" x2="78" y2="94" stroke="{LIP}" stroke-width="5" '
f'stroke-linecap="round"/>'
),
# Templates — page with horizontal lines
"folder-templates": (
f'<rect x="48" y="58" width="32" height="42" rx="2" fill="{LIP}"/>'
f'<rect x="53" y="64" width="22" height="2" fill="{BODY}"/>'
f'<rect x="53" y="70" width="22" height="2" fill="{BODY}"/>'
f'<rect x="53" y="76" width="18" height="2" fill="{BODY}"/>'
f'<rect x="53" y="82" width="20" height="2" fill="{BODY}"/>'
f'<rect x="53" y="88" width="14" height="2" fill="{BODY}"/>'
),
# Videos — play triangle
"folder-videos": (
f'<path d="M 54 62 L 54 98 L 82 80 Z" fill="{LIP}"/>'
),
}
def nexus_core_svg() -> str:
"""folder-nexus-core embeds the bicolor N logo via base64 data URI."""
return dedent(f"""\
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
width="128" height="128" viewBox="0 0 128 128">
<path d="M 14 24 Q 14 20 18 20 L 50 20 Q 53 20 55 22 L 62 30 L 110 30
Q 114 30 114 34 L 114 110 Q 114 114 110 114 L 18 114
Q 14 114 14 110 Z" fill="{TAB}"/>
<rect x="14" y="36" width="100" height="3" fill="{LIP}"/>
<path d="M 14 39 L 114 39 L 114 110 Q 114 114 110 114 L 18 114
Q 14 114 14 110 Z" fill="{BODY}"/>
<rect x="14" y="39" width="100" height="2" fill="{HIGHLIGHT}"/>
<image xlink:href="{logo_data_uri()}" x="32" y="58" width="64" height="37"
preserveAspectRatio="xMidYMid meet"/>
</svg>
""")
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())