diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b021c98..0695e01 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,6 +19,9 @@ jobs: - name: Install XcodeGen run: brew install xcodegen + - name: Generate App Icons + run: swift Scripts/generate-icon.swift + - name: Generate Xcode Project run: xcodegen generate @@ -152,39 +155,6 @@ jobs: if: github.event_name == 'pull_request' run: swift Scripts/generate-assets.swift .build/assets - - name: Inject App Icons - if: github.event_name == 'pull_request' - run: | - APP=".build/ReleaseDerivedData/Build/Products/Release/Blip.app" - HELPER_APP=".build/ReleaseDerivedData/Build/Products/Release/Blip Helper.app" - - # Blip icon - if [[ -f ".build/assets/Blip.icns" ]]; then - mkdir -p "$APP/Contents/Resources" - cp ".build/assets/Blip.icns" "$APP/Contents/Resources/AppIcon.icns" - /usr/libexec/PlistBuddy -c "Set :CFBundleIconFile AppIcon" "$APP/Contents/Info.plist" 2>/dev/null || \ - /usr/libexec/PlistBuddy -c "Add :CFBundleIconFile string AppIcon" "$APP/Contents/Info.plist" - fi - - # Helper icon - if [[ -d "$HELPER_APP" && -f ".build/assets/BlipHelper.icns" ]]; then - mkdir -p "$HELPER_APP/Contents/Resources" - cp ".build/assets/BlipHelper.icns" "$HELPER_APP/Contents/Resources/AppIcon.icns" - /usr/libexec/PlistBuddy -c "Set :CFBundleIconFile AppIcon" "$HELPER_APP/Contents/Info.plist" 2>/dev/null || \ - /usr/libexec/PlistBuddy -c "Add :CFBundleIconFile string AppIcon" "$HELPER_APP/Contents/Info.plist" - fi - - - name: Re-sign After Icon Injection - if: github.event_name == 'pull_request' - run: | - codesign --force --deep --sign "$IDENTITY" --timestamp --options runtime \ - ".build/ReleaseDerivedData/Build/Products/Release/Blip.app" - - HELPER_APP=".build/ReleaseDerivedData/Build/Products/Release/Blip Helper.app" - if [ -d "$HELPER_APP" ]; then - codesign --force --deep --sign "$IDENTITY" --timestamp --options runtime "$HELPER_APP" - fi - - name: Package Pre-release DMGs if: github.event_name == 'pull_request' run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 43bbe22..75c2bd2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,6 +19,9 @@ jobs: - name: Install XcodeGen run: brew install xcodegen + - name: Generate App Icons + run: swift Scripts/generate-icon.swift + - name: Generate Xcode Project run: xcodegen generate diff --git a/Blip/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon.png b/Blip/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon.png new file mode 100644 index 0000000..c121dea Binary files /dev/null and b/Blip/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon.png differ diff --git a/Blip/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/Blip/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json index 6829957..29f290b 100644 --- a/Blip/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/Blip/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,64 +1,10 @@ { "images" : [ { - "filename" : "icon_16x16_1x.png", - "idiom" : "mac", - "scale" : "1x", - "size" : "16x16" - }, - { - "filename" : "icon_16x16_2x.png", - "idiom" : "mac", - "scale" : "2x", - "size" : "16x16" - }, - { - "filename" : "icon_32x32_1x.png", - "idiom" : "mac", - "scale" : "1x", - "size" : "32x32" - }, - { - "filename" : "icon_32x32_2x.png", - "idiom" : "mac", - "scale" : "2x", - "size" : "32x32" - }, - { - "filename" : "icon_128x128_1x.png", - "idiom" : "mac", - "scale" : "1x", - "size" : "128x128" - }, - { - "filename" : "icon_128x128_2x.png", - "idiom" : "mac", - "scale" : "2x", - "size" : "128x128" - }, - { - "filename" : "icon_256x256_1x.png", - "idiom" : "mac", - "scale" : "1x", - "size" : "256x256" - }, - { - "filename" : "icon_256x256_2x.png", - "idiom" : "mac", - "scale" : "2x", - "size" : "256x256" - }, - { - "filename" : "icon_512x512_1x.png", - "idiom" : "mac", - "scale" : "1x", - "size" : "512x512" - }, - { - "filename" : "icon_512x512_2x.png", - "idiom" : "mac", - "scale" : "2x", - "size" : "512x512" + "filename" : "AppIcon.png", + "idiom" : "universal", + "platform" : "mac", + "size" : "1024x1024" } ], "info" : { diff --git a/BlipHelper/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon.png b/BlipHelper/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon.png new file mode 100644 index 0000000..32f6f36 Binary files /dev/null and b/BlipHelper/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon.png differ diff --git a/BlipHelper/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/BlipHelper/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json index 6829957..29f290b 100644 --- a/BlipHelper/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/BlipHelper/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,64 +1,10 @@ { "images" : [ { - "filename" : "icon_16x16_1x.png", - "idiom" : "mac", - "scale" : "1x", - "size" : "16x16" - }, - { - "filename" : "icon_16x16_2x.png", - "idiom" : "mac", - "scale" : "2x", - "size" : "16x16" - }, - { - "filename" : "icon_32x32_1x.png", - "idiom" : "mac", - "scale" : "1x", - "size" : "32x32" - }, - { - "filename" : "icon_32x32_2x.png", - "idiom" : "mac", - "scale" : "2x", - "size" : "32x32" - }, - { - "filename" : "icon_128x128_1x.png", - "idiom" : "mac", - "scale" : "1x", - "size" : "128x128" - }, - { - "filename" : "icon_128x128_2x.png", - "idiom" : "mac", - "scale" : "2x", - "size" : "128x128" - }, - { - "filename" : "icon_256x256_1x.png", - "idiom" : "mac", - "scale" : "1x", - "size" : "256x256" - }, - { - "filename" : "icon_256x256_2x.png", - "idiom" : "mac", - "scale" : "2x", - "size" : "256x256" - }, - { - "filename" : "icon_512x512_1x.png", - "idiom" : "mac", - "scale" : "1x", - "size" : "512x512" - }, - { - "filename" : "icon_512x512_2x.png", - "idiom" : "mac", - "scale" : "2x", - "size" : "512x512" + "filename" : "AppIcon.png", + "idiom" : "universal", + "platform" : "mac", + "size" : "1024x1024" } ], "info" : { diff --git a/Scripts/generate-assets.swift b/Scripts/generate-assets.swift index 0e53d12..b89a2ec 100755 --- a/Scripts/generate-assets.swift +++ b/Scripts/generate-assets.swift @@ -83,10 +83,9 @@ func drawIcon(size: CGFloat) -> NSImage { image.lockFocus() let rect = NSRect(x: 0, y: 0, width: size, height: size) - let cornerRadius = size * 0.22 - // Background gradient: deep navy to dark blue - let path = NSBezierPath(roundedRect: rect, xRadius: cornerRadius, yRadius: cornerRadius) + // Full-bleed square background — macOS 26 clips the icon shape itself + let path = NSBezierPath(rect: rect) let gradient = NSGradient( colors: [brandDarkNavy, brandDeepBlue], atLocations: [0.0, 1.0], @@ -224,10 +223,9 @@ func drawHelperIcon(size: CGFloat) -> NSImage { image.lockFocus() let rect = NSRect(x: 0, y: 0, width: size, height: size) - let cornerRadius = size * 0.22 - // Background gradient: slightly warmer deep navy - let path = NSBezierPath(roundedRect: rect, xRadius: cornerRadius, yRadius: cornerRadius) + // Full-bleed square background — macOS 26 clips the icon shape itself + let path = NSBezierPath(rect: rect) let gradient = NSGradient( colors: [ NSColor(red: 0.06, green: 0.06, blue: 0.16, alpha: 1.0), diff --git a/Scripts/generate-icon.swift b/Scripts/generate-icon.swift index 7066f0d..4afec72 100644 --- a/Scripts/generate-icon.swift +++ b/Scripts/generate-icon.swift @@ -1,12 +1,14 @@ #!/usr/bin/env swift -// Generates Blip app icon at all required macOS sizes. -// Design: Dark rounded-rect background with 3 colored horizontal bars (CPU/MEM/DISK) -// and a small radar "blip" dot — matching the menu bar aesthetic. +// Generates Blip and BlipHelper app icons for macOS 26 xcassets format. +// macOS 26: single 1024×1024 full-bleed PNG — the system clips the shape. +// Run from the repo root: swift Scripts/generate-icon.swift import Cocoa -func generateIcon(size: Int) -> NSImage { - let s = CGFloat(size) +// MARK: - Blip icon (bars + radar dot) + +func drawBlipIcon(size: CGFloat) -> NSImage { + let s = size let image = NSImage(size: NSSize(width: s, height: s)) image.lockFocus() @@ -15,94 +17,176 @@ func generateIcon(size: Int) -> NSImage { return image } - // Background: dark rounded rect + // Full-bleed square background — macOS 26 clips the icon shape itself let bgRect = CGRect(x: 0, y: 0, width: s, height: s) - let cornerRadius = s * 0.22 - let bgPath = CGPath(roundedRect: bgRect, cornerWidth: cornerRadius, cornerHeight: cornerRadius, transform: nil) - ctx.setFillColor(NSColor(red: 0.12, green: 0.12, blue: 0.14, alpha: 1).cgColor) - ctx.addPath(bgPath) - ctx.fillPath() + ctx.setFillColor(NSColor(red: 0.05, green: 0.08, blue: 0.18, alpha: 1).cgColor) + ctx.fill(bgRect) - // Subtle gradient overlay for depth + // Subtle gradient overlay for depth (clipped to square, not rounded rect) let gradientColors = [ NSColor(white: 1, alpha: 0.06).cgColor, NSColor(white: 0, alpha: 0.05).cgColor, ] as CFArray - if let gradient = CGGradient(colorsSpace: CGColorSpaceCreateDeviceRGB(), colors: gradientColors, locations: [0, 1]) { + if let gradient = CGGradient( + colorsSpace: CGColorSpaceCreateDeviceRGB(), + colors: gradientColors, + locations: [0, 1] + ) { ctx.saveGState() - ctx.addPath(bgPath) - ctx.clip() - ctx.drawLinearGradient(gradient, start: CGPoint(x: s / 2, y: s), end: CGPoint(x: s / 2, y: 0), options: []) + ctx.clip(to: bgRect) + ctx.drawLinearGradient( + gradient, + start: CGPoint(x: s / 2, y: s), + end: CGPoint(x: s / 2, y: 0), + options: [] + ) ctx.restoreGState() } - // Bar layout parameters - let barHeight = s * 0.065 + // Bar layout + let barHeight = s * 0.065 let barSpacing = s * 0.045 - let barCorner = barHeight / 2 - let barLeftX = s * 0.22 - let barWidth = s * 0.56 + let barCorner = barHeight / 2 + let barLeftX = s * 0.22 + let barWidth = s * 0.56 - // Three bars centered vertically let totalBarsHeight = 3 * barHeight + 2 * barSpacing - let barsStartY = (s - totalBarsHeight) / 2 + totalBarsHeight // flip for CG coords - - struct BarInfo { - let fill: CGFloat // fill percentage - let r: CGFloat; let g: CGFloat; let b: CGFloat // color - } + let barsStartY = (s - totalBarsHeight) / 2 + totalBarsHeight + struct BarInfo { let fill: CGFloat; let r, g, b: CGFloat } let bars: [BarInfo] = [ - BarInfo(fill: 0.55, r: 0.25, g: 0.52, b: 1.0), // CPU — blue - BarInfo(fill: 0.72, r: 0.30, g: 0.78, b: 0.40), // MEM — green - BarInfo(fill: 0.40, r: 1.0, g: 0.58, b: 0.20), // DISK — orange + BarInfo(fill: 0.55, r: 0.25, g: 0.52, b: 1.0), // CPU — blue + BarInfo(fill: 0.72, r: 0.30, g: 0.78, b: 0.40), // MEM — green + BarInfo(fill: 0.40, r: 1.0, g: 0.58, b: 0.20), // DISK — orange ] for (i, bar) in bars.enumerated() { let y = barsStartY - CGFloat(i) * (barHeight + barSpacing) - barHeight - // Track (dim background) - let trackRect = CGRect(x: barLeftX, y: y, width: barWidth, height: barHeight) - let trackPath = CGPath(roundedRect: trackRect, cornerWidth: barCorner, cornerHeight: barCorner, transform: nil) + let trackPath = CGPath( + roundedRect: CGRect(x: barLeftX, y: y, width: barWidth, height: barHeight), + cornerWidth: barCorner, cornerHeight: barCorner, transform: nil + ) ctx.setFillColor(NSColor(red: bar.r, green: bar.g, blue: bar.b, alpha: 0.2).cgColor) - ctx.addPath(trackPath) - ctx.fillPath() + ctx.addPath(trackPath); ctx.fillPath() - // Fill - let fillWidth = barWidth * bar.fill - let fillRect = CGRect(x: barLeftX, y: y, width: fillWidth, height: barHeight) - let fillPath = CGPath(roundedRect: fillRect, cornerWidth: barCorner, cornerHeight: barCorner, transform: nil) + let fillPath = CGPath( + roundedRect: CGRect(x: barLeftX, y: y, width: barWidth * bar.fill, height: barHeight), + cornerWidth: barCorner, cornerHeight: barCorner, transform: nil + ) ctx.setFillColor(NSColor(red: bar.r, green: bar.g, blue: bar.b, alpha: 1.0).cgColor) - ctx.addPath(fillPath) - ctx.fillPath() + ctx.addPath(fillPath); ctx.fillPath() } - // Blip dot — a small glowing circle in the upper right area - let dotRadius = s * 0.045 - let dotX = s * 0.74 - let dotY = s * 0.72 - let dotRect = CGRect(x: dotX - dotRadius, y: dotY - dotRadius, width: dotRadius * 2, height: dotRadius * 2) + // Radar dot with glow + let dotRadius = s * 0.075 + let dotCenter = CGPoint(x: s * 0.5, y: s * 0.72) + let dotRect = CGRect( + x: dotCenter.x - dotRadius, y: dotCenter.y - dotRadius, + width: dotRadius * 2, height: dotRadius * 2 + ) + + // Outer glow rings + let cyanGlow = NSColor(red: 0.2, green: 0.8, blue: 1.0, alpha: 0.4) + for multiplier: CGFloat in [3.0, 2.0] { + let ringSize = dotRadius * 2 * multiplier + let ringRect = CGRect( + x: dotCenter.x - ringSize / 2, y: dotCenter.y - ringSize / 2, + width: ringSize, height: ringSize + ) + NSGradient(colors: [cyanGlow, cyanGlow.withAlphaComponent(0.0)])! + .draw(in: NSBezierPath(ovalIn: ringRect), relativeCenterPosition: .zero) + } - // Glow + // Glow pass ctx.saveGState() - ctx.setShadow(offset: .zero, blur: s * 0.04, color: NSColor(red: 0.3, green: 0.85, blue: 0.4, alpha: 0.8).cgColor) - ctx.setFillColor(NSColor(red: 0.3, green: 0.85, blue: 0.4, alpha: 1).cgColor) + ctx.setShadow( + offset: .zero, blur: s * 0.06, + color: NSColor(red: 0.2, green: 0.8, blue: 1.0, alpha: 0.9).cgColor + ) + ctx.setFillColor(NSColor(red: 0.3, green: 0.9, blue: 1.0, alpha: 1).cgColor) ctx.fillEllipse(in: dotRect) ctx.restoreGState() - // Dot again on top (crisp) - ctx.setFillColor(NSColor(red: 0.3, green: 0.85, blue: 0.4, alpha: 1).cgColor) + // Crisp dot on top + ctx.setFillColor(NSColor(red: 0.3, green: 0.9, blue: 1.0, alpha: 1).cgColor) ctx.fillEllipse(in: dotRect) image.unlockFocus() return image } -func savePNG(_ image: NSImage, to path: String, pixelSize: Int) { +// MARK: - BlipHelper icon (lightning bolt) + +func drawHelperIcon(size: CGFloat) -> NSImage { + let image = NSImage(size: NSSize(width: size, height: size)) + image.lockFocus() + + let rect = NSRect(x: 0, y: 0, width: size, height: size) + + // Full-bleed square background + NSGradient( + colors: [ + NSColor(red: 0.06, green: 0.06, blue: 0.16, alpha: 1.0), + NSColor(red: 0.12, green: 0.10, blue: 0.28, alpha: 1.0), + ], + atLocations: [0.0, 1.0], + colorSpace: .deviceRGB + )!.draw(in: NSBezierPath(rect: rect), angle: -45) + + let boltColor = NSColor(red: 1.0, green: 0.8, blue: 0.2, alpha: 1.0) + let boltGlow = NSColor(red: 1.0, green: 0.8, blue: 0.2, alpha: 0.3) + + let glowSize = size * 0.75 + NSGradient(colors: [boltGlow, boltGlow.withAlphaComponent(0.0)])! + .draw( + in: NSBezierPath(ovalIn: NSRect( + x: (size - glowSize) / 2, y: (size - glowSize) / 2, + width: glowSize, height: glowSize + )), + relativeCenterPosition: .zero + ) + + let bolt = NSBezierPath() + let cx = size * 0.5 + bolt.move(to: NSPoint(x: cx - size * 0.03, y: size * 0.92)) + bolt.line(to: NSPoint(x: cx - size * 0.18, y: size * 0.55)) + bolt.line(to: NSPoint(x: cx + size * 0.03, y: size * 0.55 + size * 0.06)) + bolt.line(to: NSPoint(x: cx + size * 0.03, y: size * 0.22)) + bolt.line(to: NSPoint(x: cx + size * 0.18, y: size * 0.50)) + bolt.line(to: NSPoint(x: cx - size * 0.03, y: size * 0.44)) + bolt.close() + boltColor.setFill() + bolt.fill() + + let barH = size * 0.045 + let barX = size * 0.2 + let barY = size * 0.10 + + NSColor(red: 1.0, green: 0.8, blue: 0.2, alpha: 0.15).setFill() + NSBezierPath( + roundedRect: NSRect(x: barX, y: barY, width: size * 0.6, height: barH), + xRadius: barH / 2, yRadius: barH / 2 + ).fill() + + boltColor.withAlphaComponent(0.6).setFill() + NSBezierPath( + roundedRect: NSRect(x: barX, y: barY, width: size * 0.6 * 0.7, height: barH), + xRadius: barH / 2, yRadius: barH / 2 + ).fill() + + image.unlockFocus() + return image +} + +// MARK: - Save helpers + +func savePNG(_ image: NSImage, to path: String) { + let size = image.size let rep = NSBitmapImageRep( bitmapDataPlanes: nil, - pixelsWide: pixelSize, - pixelsHigh: pixelSize, + pixelsWide: Int(size.width), + pixelsHigh: Int(size.height), bitsPerSample: 8, samplesPerPixel: 4, hasAlpha: true, @@ -111,65 +195,45 @@ func savePNG(_ image: NSImage, to path: String, pixelSize: Int) { bytesPerRow: 0, bitsPerPixel: 0 )! - rep.size = image.size + rep.size = size NSGraphicsContext.saveGraphicsState() NSGraphicsContext.current = NSGraphicsContext(bitmapImageRep: rep) - image.draw(in: NSRect(origin: .zero, size: image.size)) + image.draw(in: NSRect(origin: .zero, size: size)) NSGraphicsContext.restoreGraphicsState() - - let data = rep.representation(using: .png, properties: [:])! - try! data.write(to: URL(fileURLWithPath: path)) + try! rep.representation(using: .png, properties: [:])! + .write(to: URL(fileURLWithPath: path)) } -// Generate all required sizes -let outputDir = "Blip/Resources/Assets.xcassets/AppIcon.appiconset" - -struct IconSize { - let points: Int - let scale: Int - var pixels: Int { points * scale } - var filename: String { "icon_\(points)x\(points)@\(scale)x.png" } +func writeContentsJSON(to dir: String) { + let contents: [String: Any] = [ + "images": [ + [ + "filename": "AppIcon.png", + "idiom": "universal", + "platform": "mac", + "size": "1024x1024", + ] + ], + "info": ["author": "xcode", "version": 1], + ] + let data = try! JSONSerialization.data( + withJSONObject: contents, + options: [.prettyPrinted, .sortedKeys] + ) + try! data.write(to: URL(fileURLWithPath: "\(dir)/Contents.json")) } -let sizes: [IconSize] = [ - IconSize(points: 16, scale: 1), - IconSize(points: 16, scale: 2), - IconSize(points: 32, scale: 1), - IconSize(points: 32, scale: 2), - IconSize(points: 128, scale: 1), - IconSize(points: 128, scale: 2), - IconSize(points: 256, scale: 1), - IconSize(points: 256, scale: 2), - IconSize(points: 512, scale: 1), - IconSize(points: 512, scale: 2), -] - -for iconSize in sizes { - let icon = generateIcon(size: iconSize.pixels) - let path = "\(outputDir)/\(iconSize.filename)" - savePNG(icon, to: path, pixelSize: iconSize.pixels) - print("Generated \(iconSize.filename) (\(iconSize.pixels)x\(iconSize.pixels)px)") -} +// MARK: - Main -// Update Contents.json -let images = sizes.map { size -> [String: String] in - [ - "filename": size.filename, - "idiom": "mac", - "scale": "\(size.scale)x", - "size": "\(size.points)x\(size.points)" - ] -} +let blipDir = "Blip/Resources/Assets.xcassets/AppIcon.appiconset" +let helperDir = "BlipHelper/Resources/Assets.xcassets/AppIcon.appiconset" -let contents: [String: Any] = [ - "images": images, - "info": [ - "author": "xcode", - "version": 1 - ] -] +savePNG(drawBlipIcon(size: 1024), to: "\(blipDir)/AppIcon.png") +writeContentsJSON(to: blipDir) +print("✓ Blip AppIcon.png → \(blipDir)") + +savePNG(drawHelperIcon(size: 1024), to: "\(helperDir)/AppIcon.png") +writeContentsJSON(to: helperDir) +print("✓ BlipHelper AppIcon.png → \(helperDir)") -let jsonData = try! JSONSerialization.data(withJSONObject: contents, options: [.prettyPrinted, .sortedKeys]) -try! jsonData.write(to: URL(fileURLWithPath: "\(outputDir)/Contents.json")) -print("Updated Contents.json") -print("Done!") +print("Done! Re-run XcodeGen if Contents.json changed.") diff --git a/ci_scripts/ci_post_clone.sh b/ci_scripts/ci_post_clone.sh index cfcd2d7..9701cde 100755 --- a/ci_scripts/ci_post_clone.sh +++ b/ci_scripts/ci_post_clone.sh @@ -16,6 +16,4 @@ echo "Generating Xcode project..." cd "$CI_PRIMARY_REPOSITORY_PATH" xcodegen generate -echo "=== Xcode project generated successfully ===" - echo "=== Post-clone complete ===" diff --git a/project.yml b/project.yml index 4cfda4a..d06ba77 100644 --- a/project.yml +++ b/project.yml @@ -3,7 +3,7 @@ options: bundleIdPrefix: com.blainemiller deploymentTarget: macOS: "14.0" - xcodeVersion: "16.3" + xcodeVersion: "26.0" settings: base: SWIFT_VERSION: "6.0"