feat(assets): NexusOS asset + theme rebrand
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+278
@@ -0,0 +1,278 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
NexusOS action-icon generator.
|
||||
|
||||
Builds the icons the XFCE logoff dialog (xfce4-session-logout) and the
|
||||
Whisker menu command buttons request — shutdown / reboot / log-out /
|
||||
suspend / hibernate / switch-user / lock-screen / settings — in the
|
||||
NexusOS palette (white glyph on a deep-purple disc with a lime ring).
|
||||
|
||||
Emits one SVG per icon into actions/, rasterizes to
|
||||
~/.icons/NexusOS/<size>/actions/<name>.png at 16/22/24/32/48/64/128,
|
||||
then writes the alias copies (xfsm-*, system-suspend-hibernate, and the
|
||||
settings-manager app-icon names that Whisker's Settings button uses).
|
||||
|
||||
Run: python3 build_actions.py # SVGs + PNGs + aliases
|
||||
python3 build_actions.py --svg-only # just rewrite SVGs
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
ACTIONS = ROOT / "actions"
|
||||
ICONS_OUT = Path.home() / ".icons" / "NexusOS"
|
||||
SIZES = [16, 22, 24, 32, 48, 64, 128]
|
||||
|
||||
# ── colors (mirror gtk-3.0/colors.css) ────────────────────────────────
|
||||
BODY = "#5e0066" # brand_purple_dark — disc fill
|
||||
RING = "#8cc63f" # brand_green — disc ring
|
||||
GLYPH = "#f2f2f2" # text_primary — glyph
|
||||
|
||||
# ── master: purple disc + lime ring, viewBox 0 0 128 128 ──────────────
|
||||
# Glyph area: a ~70px box centered on (64, 64).
|
||||
DISC = f' <circle cx="64" cy="64" r="52" fill="{BODY}" ' \
|
||||
f'stroke="{RING}" stroke-width="6"/>\n'
|
||||
|
||||
|
||||
def _gear_glyph() -> str:
|
||||
"""Settings cog: 8 teeth on a white disc with a punched centre hole."""
|
||||
teeth = "".join(
|
||||
f' <rect x="57" y="27" width="14" height="21" rx="2" '
|
||||
f'transform="rotate({k * 45} 64 64)" fill="{GLYPH}"/>\n'
|
||||
for k in range(8)
|
||||
)
|
||||
return (
|
||||
" <!-- Settings cog -->\n"
|
||||
+ teeth
|
||||
+ f' <circle cx="64" cy="64" r="25" fill="{GLYPH}"/>\n'
|
||||
+ f' <circle cx="64" cy="64" r="10" fill="{BODY}"/>\n'
|
||||
)
|
||||
|
||||
|
||||
# ── glyph fragments — white, centered on (64, 64) ─────────────────────
|
||||
GLYPHS = {
|
||||
"system-shutdown": f"""\
|
||||
<!-- Power symbol: broken ring (gap at top) + stem -->
|
||||
<path d="M 55 51 A 22 22 0 1 0 73 51" fill="none" stroke="{GLYPH}"
|
||||
stroke-width="10" stroke-linecap="round"/>
|
||||
<rect x="59" y="35" width="10" height="31" rx="5" fill="{GLYPH}"/>
|
||||
""",
|
||||
|
||||
"system-reboot": f"""\
|
||||
<!-- Circular arrow -->
|
||||
<path d="M 64 40 A 24 24 0 1 1 41 71" fill="none" stroke="{GLYPH}"
|
||||
stroke-width="9" stroke-linecap="round"/>
|
||||
<path d="M 50 32 L 70 40 L 56 56 Z" fill="{GLYPH}"/>
|
||||
""",
|
||||
|
||||
"system-log-out": f"""\
|
||||
<!-- Doorway bracket, open on the right -->
|
||||
<path d="M 72 34 L 42 34 L 42 94 L 72 94" fill="none" stroke="{GLYPH}"
|
||||
stroke-width="9" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<!-- Arrow exiting to the right -->
|
||||
<path d="M 56 64 L 92 64" fill="none" stroke="{GLYPH}"
|
||||
stroke-width="9" stroke-linecap="round"/>
|
||||
<path d="M 80 50 L 96 64 L 80 78" fill="none" stroke="{GLYPH}"
|
||||
stroke-width="9" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
""",
|
||||
|
||||
"system-suspend": f"""\
|
||||
<!-- Crescent moon: big disc minus a disc shifted right -->
|
||||
<path d="M 74 33 A 33 33 0 1 0 74 95 A 33 33 0 0 1 74 33 Z"
|
||||
fill="{GLYPH}"/>
|
||||
""",
|
||||
|
||||
"system-hibernate": f"""\
|
||||
<!-- Crescent moon + Z -->
|
||||
<path d="M 66 39 A 28 28 0 1 0 66 91 A 28 28 0 0 1 66 39 Z"
|
||||
fill="{GLYPH}"/>
|
||||
<path d="M 78 34 L 99 34 L 78 56 L 99 56" fill="none" stroke="{GLYPH}"
|
||||
stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
""",
|
||||
|
||||
"system-switch-user": f"""\
|
||||
<!-- Two person silhouettes -->
|
||||
<circle cx="78" cy="56" r="13" fill="{GLYPH}"/>
|
||||
<path d="M 56 96 Q 56 72 78 72 Q 100 72 100 96 Z" fill="{GLYPH}"/>
|
||||
<circle cx="48" cy="60" r="12" fill="{GLYPH}"/>
|
||||
<path d="M 28 96 Q 28 76 48 76 Q 60 76 65 84 L 56 88
|
||||
Q 56 84 56 96 Z" fill="{GLYPH}"/>
|
||||
""",
|
||||
|
||||
"system-lock-screen": f"""\
|
||||
<!-- Padlock -->
|
||||
<path d="M 52 64 L 52 50 A 12 12 0 0 1 76 50 L 76 64" fill="none"
|
||||
stroke="{GLYPH}" stroke-width="8" stroke-linecap="round"/>
|
||||
<rect x="40" y="62" width="48" height="40" rx="7" fill="{GLYPH}"/>
|
||||
<circle cx="64" cy="78" r="6" fill="{BODY}"/>
|
||||
<rect x="61" y="80" width="6" height="13" rx="3" fill="{BODY}"/>
|
||||
""",
|
||||
|
||||
"preferences-desktop": _gear_glyph(),
|
||||
|
||||
# ── Whisker menu application-category glyphs ──────────────────────
|
||||
"applications-internet": f"""\
|
||||
<!-- Globe -->
|
||||
<circle cx="64" cy="64" r="28" fill="none" stroke="{GLYPH}" stroke-width="7"/>
|
||||
<ellipse cx="64" cy="64" rx="12" ry="28" fill="none" stroke="{GLYPH}" stroke-width="7"/>
|
||||
<line x1="38" y1="55" x2="90" y2="55" stroke="{GLYPH}" stroke-width="7" stroke-linecap="round"/>
|
||||
<line x1="38" y1="73" x2="90" y2="73" stroke="{GLYPH}" stroke-width="7" stroke-linecap="round"/>
|
||||
""",
|
||||
|
||||
"applications-development": f"""\
|
||||
<!-- Code chevrons + slash -->
|
||||
<path d="M 52 42 L 34 64 L 52 86" fill="none" stroke="{GLYPH}"
|
||||
stroke-width="9" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 76 42 L 94 64 L 76 86" fill="none" stroke="{GLYPH}"
|
||||
stroke-width="9" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<line x1="71" y1="38" x2="57" y2="90" stroke="{GLYPH}"
|
||||
stroke-width="8" stroke-linecap="round"/>
|
||||
""",
|
||||
|
||||
"applications-multimedia": f"""\
|
||||
<!-- Play triangle -->
|
||||
<path d="M 51 40 L 51 88 L 92 64 Z" fill="{GLYPH}"/>
|
||||
""",
|
||||
|
||||
"applications-office": f"""\
|
||||
<!-- Document with folded corner + text lines -->
|
||||
<path d="M 44 32 L 74 32 L 86 44 L 86 96 L 44 96 Z" fill="{GLYPH}"/>
|
||||
<path d="M 74 32 L 74 44 L 86 44 Z" fill="{BODY}"/>
|
||||
<line x1="52" y1="58" x2="78" y2="58" stroke="{BODY}" stroke-width="5" stroke-linecap="round"/>
|
||||
<line x1="52" y1="70" x2="78" y2="70" stroke="{BODY}" stroke-width="5" stroke-linecap="round"/>
|
||||
<line x1="52" y1="82" x2="70" y2="82" stroke="{BODY}" stroke-width="5" stroke-linecap="round"/>
|
||||
""",
|
||||
|
||||
"applications-graphics": f"""\
|
||||
<!-- Framed picture: sun + mountains -->
|
||||
<rect x="34" y="40" width="60" height="48" rx="5" fill="none" stroke="{GLYPH}" stroke-width="7"/>
|
||||
<circle cx="51" cy="55" r="6" fill="{GLYPH}"/>
|
||||
<path d="M 40 84 L 56 63 L 67 74 L 78 58 L 88 84 Z" fill="{GLYPH}"/>
|
||||
""",
|
||||
|
||||
"applications-system": f"""\
|
||||
<!-- Monitor -->
|
||||
<rect x="32" y="36" width="64" height="44" rx="5" fill="{GLYPH}"/>
|
||||
<rect x="40" y="44" width="48" height="28" rx="2" fill="{BODY}"/>
|
||||
<rect x="56" y="80" width="16" height="9" fill="{GLYPH}"/>
|
||||
<rect x="44" y="89" width="40" height="7" rx="3" fill="{GLYPH}"/>
|
||||
""",
|
||||
|
||||
"applications-utilities": f"""\
|
||||
<!-- Open-end wrench -->
|
||||
<g transform="rotate(45 64 64)">
|
||||
<rect x="58" y="44" width="12" height="44" rx="5" fill="{GLYPH}"/>
|
||||
<circle cx="64" cy="44" r="14" fill="{GLYPH}"/>
|
||||
<rect x="57" y="27" width="14" height="17" fill="{BODY}"/>
|
||||
</g>
|
||||
""",
|
||||
|
||||
"applications-games": f"""\
|
||||
<!-- Gamepad -->
|
||||
<rect x="36" y="50" width="56" height="30" rx="15" fill="{GLYPH}"/>
|
||||
<rect x="48" y="62.5" width="16" height="5" rx="1" fill="{BODY}"/>
|
||||
<rect x="53.5" y="57" width="5" height="16" rx="1" fill="{BODY}"/>
|
||||
<circle cx="76" cy="60" r="3.5" fill="{BODY}"/>
|
||||
<circle cx="84" cy="68" r="3.5" fill="{BODY}"/>
|
||||
""",
|
||||
|
||||
"applications-science": f"""\
|
||||
<!-- Erlenmeyer flask -->
|
||||
<path d="M 56 34 L 56 58 L 38 88 Q 35 95 43 95 L 85 95 Q 93 95 90 88 L 72 58 L 72 34"
|
||||
fill="none" stroke="{GLYPH}" stroke-width="7" stroke-linejoin="round"/>
|
||||
<line x1="51" y1="34" x2="77" y2="34" stroke="{GLYPH}" stroke-width="8" stroke-linecap="round"/>
|
||||
<path d="M 51 76 L 77 76 L 86 88 Q 89 93 83 93 L 45 93 Q 39 93 42 88 Z" fill="{GLYPH}"/>
|
||||
""",
|
||||
}
|
||||
|
||||
# Extra filenames that render the same art (XFCE's xfsm-* lookup names,
|
||||
# the hybrid-sleep alias, and the Whisker command-button names).
|
||||
ALIASES = {
|
||||
"xfsm-logout": "system-log-out",
|
||||
"xfsm-reboot": "system-reboot",
|
||||
"xfsm-shutdown": "system-shutdown",
|
||||
"xfsm-suspend": "system-suspend",
|
||||
"xfsm-hibernate": "system-hibernate",
|
||||
"xfsm-switch-user": "system-switch-user",
|
||||
"system-suspend-hibernate": "system-suspend",
|
||||
"xfsm-lock": "system-lock-screen",
|
||||
"preferences-system": "preferences-desktop",
|
||||
"org.xfce.settings.manager": "preferences-desktop",
|
||||
"xfce4-settings-manager": "preferences-desktop",
|
||||
# XFCE's "Accessories" menu uses xfce-accessories.directory
|
||||
# (Icon=applications-accessories), NOT the freedesktop Utility name, so
|
||||
# it needs the same wrench/tools badge as applications-utilities.
|
||||
"applications-accessories": "applications-utilities",
|
||||
}
|
||||
|
||||
|
||||
def svg_for(glyph: str) -> str:
|
||||
return (
|
||||
'<?xml version="1.0" encoding="UTF-8"?>\n'
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" '
|
||||
'viewBox="0 0 128 128">\n'
|
||||
f"{DISC}{glyph}</svg>\n"
|
||||
)
|
||||
|
||||
|
||||
def write_svgs() -> list[str]:
|
||||
ACTIONS.mkdir(parents=True, exist_ok=True)
|
||||
for name, glyph in GLYPHS.items():
|
||||
(ACTIONS / f"{name}.svg").write_text(svg_for(glyph))
|
||||
return list(GLYPHS)
|
||||
|
||||
|
||||
def render_pngs(names: list[str]) -> None:
|
||||
for name in names:
|
||||
svg = ACTIONS / f"{name}.svg"
|
||||
for size in SIZES:
|
||||
out_dir = ICONS_OUT / f"{size}x{size}" / "actions"
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
out = out_dir / f"{name}.png"
|
||||
subprocess.run(
|
||||
[
|
||||
"inkscape",
|
||||
str(svg),
|
||||
"--export-type=png",
|
||||
f"--export-filename={out}",
|
||||
f"--export-width={size}",
|
||||
f"--export-height={size}",
|
||||
],
|
||||
check=True,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
print(f" rendered {name} @ {size}")
|
||||
|
||||
|
||||
def write_aliases() -> None:
|
||||
for alias, src in ALIASES.items():
|
||||
for size in SIZES:
|
||||
d = ICONS_OUT / f"{size}x{size}" / "actions"
|
||||
src_png = d / f"{src}.png"
|
||||
if src_png.exists():
|
||||
shutil.copy2(src_png, d / f"{alias}.png")
|
||||
print(f" alias {alias} -> {src}")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
svg_only = "--svg-only" in sys.argv
|
||||
names = write_svgs()
|
||||
print(f"Wrote {len(names)} SVGs into {ACTIONS}")
|
||||
if svg_only:
|
||||
return
|
||||
render_pngs(names)
|
||||
write_aliases()
|
||||
subprocess.run(
|
||||
["gtk-update-icon-cache", "-f", str(ICONS_OUT)],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
print("\nDone. Reload: xfdesktop --reload & xfce4-panel -r")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user