#!/usr/bin/env python3 """Render native icons into memory and inspect pixels; no app, windows or screenshots.""" from pathlib import Path import subprocess import tempfile ROOT = Path(__file__).resolve().parents[1] with tempfile.TemporaryDirectory(prefix="lemana-icons-") as temporary: folder = Path(temporary) source = folder / "IconChecks.swift" source.write_text(r''' import AppKit func pixels(size: Int = 96, _ draw: (CGContext) -> Void) -> [UInt8] { let context = CGContext(data: nil, width: size, height: size, bitsPerComponent: 8, bytesPerRow: size * 4, space: CGColorSpace(name: CGColorSpace.sRGB)!, bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue | CGBitmapInfo.byteOrder32Big.rawValue)! context.translateBy(x: 0, y: CGFloat(size)) context.scaleBy(x: CGFloat(size) / 24, y: -CGFloat(size) / 24) draw(context) return Array(UnsafeBufferPointer(start: context.data!.assumingMemoryBound(to: UInt8.self), count: size * size * 4)) } func session(_ style: VPNIndicatorStyle, _ progress: Double?, dark: Bool, urgency: SessionTiming.Urgency = .normal) -> [UInt8] { pixels { VPNIcons.drawSession(style: style, progress: progress, urgency: urgency, in: $0, dark: dark) } } func pixel(_ bytes: [UInt8], _ x: Int, _ y: Int) -> [UInt8] { let index = (y * 4 * 96 + x * 4) * 4 return Array(bytes[index..() for icon in VPNIcon.allCases { let bytes = pixels { VPNIcons.draw(icon, in: $0, dark: dark) } precondition(bytes.contains(where: { $0 != 0 }), "empty icon: \(icon)") precondition(seen.insert(bytes).inserted, "duplicate icon: \(icon)") for size in [16, 32] { let compact = pixels(size: size) { VPNIcons.draw(icon, in: $0, dark: dark) } let count = stride(from: 3, to: compact.count, by: 4).filter { compact[$0] > 30 }.count precondition(count > 10, "invisible icon at \(size): \(icon)") } } for style in [VPNIndicatorStyle.shield, .ring] { let full = session(style, 0, dark: dark) let quarter = session(style, 0.75, dark: dark) let half = session(style, 0.5, dark: dark) let spent = session(style, 1, dark: dark) precondition(pixel(full, 12, 8)[3] > 200, "session must start brightly filled") precondition((75...80).contains(pixel(spent, 12, 18)[3]), "spent interior must stay visible") precondition(pixel(half, 7, 10) == pixel(spent, 7, 10), "spent part must dim from top") precondition(pixel(half, 12, 18) == pixel(full, 12, 18), "remaining part must stay bright") precondition(pixel(quarter, 12, 18) == pixel(half, 12, 18)) precondition(pixel(quarter, 7, 14) == pixel(spent, 7, 14)) precondition(pixel(full, 12, 18) == pixel(half, 12, 18)) precondition(pixel(full, 1, 1)[3] == 0, "fill escaped its shape") precondition(session(style, -1, dark: dark) == full) precondition(session(style, 2, dark: dark) == spent) precondition(session(style, nil, dark: dark) == pixels { VPNIcons.draw(.noTime, in: $0, dark: dark) }) precondition(session(style, .nan, dark: dark) == session(style, nil, dark: dark)) let warning = pixel(session(style, 0.5, dark: dark, urgency: .warning), 12, 18) let critical = pixel(session(style, 0.5, dark: dark, urgency: .critical), 12, 18) precondition(warning != pixel(half, 12, 18) && warning != critical, "urgency colors must differ") // Expiry stays legible over the dimmed interior in either theme. let expired = session(style, 1, dark: dark, urgency: .critical) precondition(pixel(expired, 12, 8)[3] > pixel(expired, 12, 18)[3]) } precondition(session(.clock, 0.25, dark: dark) != session(.clock, 0.75, dark: dark)) precondition(session(.time, 0.5, dark: dark) != session(.time, 1, dark: dark)) } print("PASS native icons: both themes, distinct states/actions, 16/32 px visibility, bright remaining fill, dim spent area, clipping, urgency, expiry and unknown time") } } ''') binary = folder / "icon-check" subprocess.run(["swiftc", "-parse-as-library", "-module-cache-path", str(ROOT / "app/.build/log-test-module-cache"), str(ROOT / "app/Sources/LemanaVPN/SessionTiming.swift"), str(ROOT / "app/Sources/LemanaVPN/VPNIcons.swift"), str(source), "-o", str(binary)], check=True) subprocess.run([str(binary)], check=True)