#!/usr/bin/env python3 """ NexusOS network status icons generator. Generates flat symbolic-style SVGs for nm-tray and any other Qt/GTK app that requests network-* status icons. Output goes to scalable/status/ of the NexusOS-icons theme. Design language: * 16×16 viewBox, scalable SVG (no rasterization needed — Qt5 + GTK3 both handle SVG fine in scalable/ dirs) * Light off-white stroke for base shape (wifi fan, ethernet plug, globe) — readable on the dark NexusOS surfaces * Lime brand accent for connected / active states * Purple lock for encryption * Red for error, orange for acquiring / no-route Run: python3 build_status.py """ from pathlib import Path import math ROOT = Path(__file__).resolve().parent OUT = ROOT.parent / "NexusOS-icons" / "scalable" / "status" # Palette — mirrors gtk-3.0/colors.css. BASE = "#f2f2f2" # text_primary MUTED = "#6e7173" # text_disabled GOOD = "#8cc63f" # brand_green LOCK = "#f2f2f2" # text_primary (white) — neutral attribute indicator # while green is reserved for active signal strength. ERROR = "#da4453" # error_color WARN = "#f67400" # warning_color SW = 1.6 # Wifi fan arc geometry: half-angle from vertical. Smaller half-angle ⇒ # narrower sweep but allows bigger radius (taller fan) before endpoints # run off the sides at 16x16. 40° (80° total sweep) is a sweet spot. ARC_HALF_ANGLE = 40 S_ARC = math.sin(math.radians(ARC_HALF_ANGLE)) # x offset coefficient C_ARC = math.cos(math.radians(ARC_HALF_ANGLE)) # y offset coefficient def svg(body: str) -> str: return ( '\n' + body.rstrip() + "\n\n" ) # ── wifi fan ──────────────────────────────────────────────────────── # Fan sits low in the viewBox with arcs reaching almost the top edge so # the rendered icon matches the height of neighboring tray icons (shield, # bluetooth, etc.). Outermost r=11.5 with the narrower 80° arc sweep # keeps endpoints inside x=[0.61, 15.39] — fits with stroke-cap margin. WIFI_CX, WIFI_CY = 8, 13.0 WIFI_R = [3.0, 5.5, 8.5, 11.5] DOT_R = 1.3 def _arc(r: float, color: str, opacity: float = 1.0) -> str: x0 = WIFI_CX - r * S_ARC x1 = WIFI_CX + r * S_ARC y = WIFI_CY - r * C_ARC return ( f' ' ) def wifi_fan(level: int, active_color: str = GOOD) -> str: """level 0 (none) .. 4 (excellent). Filled arcs use brand_green by default so signal strength reads as the primary visual element.""" parts = [] for i, r in enumerate(WIFI_R): if i < level: parts.append(_arc(r, active_color, 1.0)) else: parts.append(_arc(r, MUTED, 0.35)) if level > 0: parts.append( f' ' ) else: parts.append( f' ' ) return "\n".join(parts) # ── ethernet plug (RJ45) ──────────────────────────────────────────── # Sized to fill ~75% of the viewBox vertically (y=2→14) and horizontally # (x=2→14) so the rendered icon matches the height of neighboring tray # icons (shield, notification, bluetooth) instead of looking like a tiny # crate in the middle of the slot. def ethernet_plug(color: str = GOOD, opacity: float = 1.0) -> str: return ( f' \n' # Pentagonal RJ45 outline ' \n' # Three pins, spread wider to match the bigger body ' \n' ' \n' ' \n' " " ) # ── globe (generic network) ───────────────────────────────────────── def globe(color: str = BASE, opacity: float = 1.0) -> str: return ( f' \n' ' \n' ' \n' ' \n' " " ) # ── key (vpn) ─────────────────────────────────────────────────────── def vpn_key(color: str = GOOD, opacity: float = 1.0) -> str: """Horizontal key: round bow on the left, shaft to the right with two teeth hanging down. Stroke-only so it doesn't visually dominate the way the filled globe did.""" return ( f' \n' # Bow (handle) — outline circle on the left ' \n' # Shaft ' \n' # Two teeth pointing down ' \n' " " ) # ── state overlays / badges ───────────────────────────────────────── # nm-tray's MultiIconDelegate stacks state icons on top of the signal- # level base, so each overlay icon is *just the badge* — no wifi fan, # no globe. That way stacking signal-good + encrypted gives fan + lock # instead of fan + fan + lock, and adding -disconnected on top of an # unsaved network adds a single X instead of another fan. def standalone_lock(color: str = LOCK) -> str: """Centered padlock filling most of the 16x16 box. This is the icon nm-tray actually loads for encryption — it requests `security-high` / `security-high-symbolic` (NOT `network-wireless-encrypted`) and overlays it on top of a `network-wireless-signal-*` base via its MultiIconDelegate, scaling our full-size lock into a corner badge. Keyhole detail intentionally omitted: at the corner-badge render size nm-tray uses, it's invisible anyway, and a dark keyhole on a white body reads as visual noise rather than as a lock detail.""" return ( # Body: 8.5w x 6h, centered horizontally, sitting low f' \n' # Shackle: pronounced U above the body f' ' ) def badge_lock() -> str: """Lock badge: body 4.4w × 3.8h + shackle 3w × 2h on top, lower-right corner. Made deliberately larger and with a thicker shackle stroke than a typical badge — at 16-24px render, a small thin-stroke lock blurs into a shield-like rounded shape. The big-shackle proportions here keep the lock recognizable down to ~18px icon size.""" return ( ' \n' f' \n' f' \n' " " ) def badge_x(color: str = ERROR) -> str: return ( f' \n' ' \n' ' \n' " " ) def badge_dots(color: str = WARN) -> str: return ( f' \n' ' \n' ' \n' ' \n' " " ) def badge_excl(color: str = ERROR) -> str: return ( f' \n' ' \n' ' \n' " " ) def badge_slash(color: str = MUTED) -> str: return ( f' ' ) def badge_arrow_up(color: str = GOOD) -> str: return ( f' ' ) def badge_arrow_down(color: str = GOOD) -> str: return ( f' ' ) # ── icon catalog ──────────────────────────────────────────────────── def cell_tech(label: str) -> str: """Wifi fan + tiny generation-tech label (2G/3G/4G/5G/E/G/H/U/1x).""" return svg( wifi_fan(3) + "\n" f' {label}' ) ICONS = { # wireless signal levels "network-wireless-signal-none": svg(wifi_fan(0)), "network-wireless-signal-weak": svg(wifi_fan(1)), "network-wireless-signal-ok": svg(wifi_fan(2)), "network-wireless-signal-good": svg(wifi_fan(3)), "network-wireless-signal-excellent": svg(wifi_fan(4)), # wireless — nm-tray picks one icon per row, so each name is a # self-contained composite (signal-level fan + state badge if any). "network-wireless": svg(wifi_fan(3)), "network-wireless-connected": svg(wifi_fan(4, GOOD)), # Disconnected: faded-empty fan. NO X badge — nm-tray overlays this # on top of saved-network rows, so an X here would visually pile up # with the encrypted-lock overlay. The dim level-0 fan already reads # as "no signal/disconnected" without the extra badge. "network-wireless-disconnected": svg(wifi_fan(0)), "network-wireless-disabled": svg(wifi_fan(0) + "\n" + badge_slash()), "network-wireless-acquiring": svg(wifi_fan(2) + "\n" + badge_dots()), "network-wireless-error": svg(wifi_fan(2) + "\n" + badge_excl()), "network-wireless-no-route": svg(wifi_fan(3) + "\n" + badge_excl(WARN)), "network-wireless-offline": svg(wifi_fan(0)), "network-wireless-encrypted": svg(wifi_fan(3) + "\n" + badge_lock()), "network-wireless-hotspot": svg(wifi_fan(4, GOOD) + "\n" + badge_dots(GOOD)), "network-wireless-hardware-disabled": svg(wifi_fan(0) + "\n" + badge_slash(ERROR)), # wired "network-wired": svg(ethernet_plug()), "network-wired-disconnected": svg(ethernet_plug(MUTED, 0.55) + "\n" + badge_x()), "network-wired-acquiring": svg(ethernet_plug() + "\n" + badge_dots()), "network-wired-error": svg(ethernet_plug() + "\n" + badge_excl()), "network-wired-no-route": svg(ethernet_plug() + "\n" + badge_excl(WARN)), "network-wired-offline": svg(ethernet_plug(MUTED, 0.55)), # vpn — green key. Stroke-only so it doesn't dominate the menu row. "network-vpn": svg(vpn_key()), "network-vpn-connected": svg(vpn_key()), "network-vpn-acquiring": svg(vpn_key() + "\n" + badge_dots()), "network-vpn-disconnected": svg(vpn_key(MUTED, 0.55) + "\n" + badge_x()), "network-vpn-error": svg(vpn_key(ERROR)), "network-vpn-no-route": svg(vpn_key() + "\n" + badge_excl(WARN)), "network-vpn-offline": svg(vpn_key(MUTED, 0.55)), # generic "network-error": svg(globe() + "\n" + badge_excl()), "network-idle": svg(globe()), "network-offline": svg(globe(MUTED, 0.55) + "\n" + badge_slash()), "network-no-route": svg(globe() + "\n" + badge_excl(WARN)), "network-acquiring": svg(globe() + "\n" + badge_dots()), "network-transmit": svg(globe() + "\n" + badge_arrow_up()), "network-receive": svg(globe() + "\n" + badge_arrow_down()), "network-transmit-receive": svg(globe() + "\n" + badge_arrow_up() + "\n" + badge_arrow_down()), # cellular — same fan, with state badges "network-cellular-signal-none": svg(wifi_fan(0)), "network-cellular-signal-weak": svg(wifi_fan(1)), "network-cellular-signal-ok": svg(wifi_fan(2)), "network-cellular-signal-good": svg(wifi_fan(3)), "network-cellular-signal-excellent": svg(wifi_fan(4)), "network-cellular-acquiring": svg(wifi_fan(2) + "\n" + badge_dots()), "network-cellular-connected": svg(wifi_fan(4, GOOD)), "network-cellular-disabled": svg(wifi_fan(0) + "\n" + badge_slash()), "network-cellular-disconnected": svg(wifi_fan(0) + "\n" + badge_x()), "network-cellular-error": svg(wifi_fan(2) + "\n" + badge_excl()), "network-cellular-hardware-disabled": svg(wifi_fan(0) + "\n" + badge_slash(ERROR)), "network-cellular-no-route": svg(wifi_fan(3) + "\n" + badge_excl(WARN)), "network-cellular-offline": svg(wifi_fan(0)), # security indicators — nm-tray's actual encryption overlay icon "security-high": svg(standalone_lock()), "security-high-symbolic": svg(standalone_lock()), "security-medium": svg(standalone_lock(WARN)), "security-medium-symbolic": svg(standalone_lock(WARN)), "security-low": svg(standalone_lock(ERROR)), "security-low-symbolic": svg(standalone_lock(ERROR)), # cellular generation-tech labels "network-cellular-2g": cell_tech("2G"), "network-cellular-3g": cell_tech("3G"), "network-cellular-4g": cell_tech("4G"), "network-cellular-5g": cell_tech("5G"), "network-cellular-edge": cell_tech("E"), "network-cellular-gprs": cell_tech("G"), "network-cellular-cdma-1x": cell_tech("1x"), "network-cellular-hspa": cell_tech("H"), "network-cellular-umts": cell_tech("U"), } # ── genmon panel applet PNGs ───────────────────────────────────────── # bin/panel/network-applet.sh (the XFCE genmon network applet) loads PNGs # directly from assets/panel-icons/ by path — it does NOT go through the # icon theme. Rasterize the relevant status icons there too so the applet # stays in sync with the canonical SVG design above (otherwise the PNGs # drift into a different look, e.g. solid filled cones vs. arc bars). PANEL_PNG_DIR = ROOT.parent.parent / "panel-icons" # assets/panel-icons PANEL_PNG_SIZE = 22 PANEL_PNG_NAMES = [ "network-wireless-signal-none", "network-wireless-signal-weak", "network-wireless-signal-ok", "network-wireless-signal-good", "network-wireless-signal-excellent", "network-wired", "network-offline", ] def _rasterize_panel_pngs() -> int: import subprocess if not PANEL_PNG_DIR.is_dir(): return 0 n = 0 for name in PANEL_PNG_NAMES: svg_path = OUT / f"{name}.svg" if not svg_path.exists(): continue subprocess.run( ["inkscape", str(svg_path), "--export-type=png", f"--export-filename={PANEL_PNG_DIR / (name + '.png')}", f"--export-width={PANEL_PNG_SIZE}", f"--export-height={PANEL_PNG_SIZE}", "--export-background-opacity=0"], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, ) n += 1 return n def main() -> None: OUT.mkdir(parents=True, exist_ok=True) # Sweep any leftover Papirus fallback symlinks from the same dir. cleaned = 0 for entry in OUT.iterdir(): if entry.is_symlink(): entry.unlink() cleaned += 1 for name, body in ICONS.items(): (OUT / f"{name}.svg").write_text(body) print(f"Cleared {cleaned} symlinks, wrote {len(ICONS)} SVGs to {OUT}") n = _rasterize_panel_pngs() print(f"Rasterized {n} genmon panel PNGs to {PANEL_PNG_DIR}") if __name__ == "__main__": main()