4a79c0072c
Docker image / Build (linux/amd64) (push) Has been cancelled
Docker image / Build (linux/arm64) (push) Has been cancelled
Docker image / Merge release multi-arch manifest (push) Has been cancelled
Docker image / Merge debug multi-arch manifest (push) Has been cancelled
Docker image / Build public push gateway (linux/amd64) (push) Has been cancelled
Docker image / Build public push gateway (linux/arm64) (push) Has been cancelled
Docker image / Publish public push gateway image (push) Has been cancelled
Harbor Buzz Orchestra / Python tests and lint (push) Has been cancelled
CI / Detect Changed Paths (push) Has been cancelled
CI / Rust Lint (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Desktop Core (push) Has been cancelled
CI / Desktop Smoke E2E (1) (push) Has been cancelled
CI / Desktop Smoke E2E (2) (push) Has been cancelled
CI / Desktop Smoke E2E (3) (push) Has been cancelled
CI / Desktop Smoke E2E (4) (push) Has been cancelled
CI / Desktop (push) Has been cancelled
CI / Desktop E2E Relay (push) Has been cancelled
CI / Desktop E2E Integration (1/2) (push) Has been cancelled
CI / Desktop E2E Integration (2/2) (push) Has been cancelled
CI / Desktop E2E Integration (push) Has been cancelled
CI / Backend Integration (relay e2e) (push) Has been cancelled
CI / Relay E2E (push) Has been cancelled
CI / Web (push) Has been cancelled
CI / Mobile (push) Has been cancelled
CI / Security (push) Has been cancelled
CI / Dead Token Reference Guard (push) Has been cancelled
CI / Server Cross-Compile (aarch64-unknown-linux-musl) (push) Has been cancelled
CI / Server Cross-Compile (x86_64-unknown-linux-musl) (push) Has been cancelled
CI / Windows Rust (x86_64-pc-windows-msvc) (push) Has been cancelled
CI / Desktop Build (macOS) (push) Has been cancelled
helm chart / lint + unittest + render matrix (push) Has been cancelled
helm chart / install on kind (gated) (push) Has been cancelled
helm chart / publish chart to GHCR (push) Has been cancelled
Mesh Lifecycle / Relay-Driven Mesh Lifecycle Smoke (push) Has been cancelled
Sprig image / Build (linux/amd64) (push) Has been cancelled
Sprig image / Build (linux/arm64) (push) Has been cancelled
Sprig image / Merge multi-arch manifest (push) Has been cancelled
Sprig / Build (aarch64-unknown-linux-musl) (push) Has been cancelled
Sprig / Build (x86_64-unknown-linux-musl) (push) Has been cancelled
Sprig / Publish rolling release (push) Has been cancelled
Sprig / Publish tagged release (push) Has been cancelled
Signed-off-by: cls_???? <908705107@qq.com>
214 lines
7.3 KiB
Swift
Executable File
214 lines
7.3 KiB
Swift
Executable File
#!/usr/bin/env swift
|
|
|
|
import AppKit
|
|
import Foundation
|
|
|
|
// Generate a dev icon with worktree name badge
|
|
// Usage: generate-dev-icon.swift <input-icns> <output-icns> <label>
|
|
|
|
guard CommandLine.arguments.count == 4 else {
|
|
fputs("Usage: \(CommandLine.arguments[0]) <input-icns> <output-icns> <label>\n", stderr)
|
|
exit(1)
|
|
}
|
|
|
|
let inputPath = CommandLine.arguments[1]
|
|
let outputPath = CommandLine.arguments[2]
|
|
let label = CommandLine.arguments[3]
|
|
|
|
func sanitizedPathComponent(_ value: String) -> String {
|
|
let lowered = value.lowercased()
|
|
let sanitizedScalars = lowered.unicodeScalars.map { scalar -> Character in
|
|
switch scalar {
|
|
case "a"..."z", "0"..."9":
|
|
return Character(scalar)
|
|
default:
|
|
return "-"
|
|
}
|
|
}
|
|
let collapsed = String(sanitizedScalars).replacingOccurrences(
|
|
of: "-+",
|
|
with: "-",
|
|
options: .regularExpression
|
|
)
|
|
let trimmed = collapsed.trimmingCharacters(in: CharacterSet(charactersIn: "-"))
|
|
return trimmed.isEmpty ? "dev" : trimmed
|
|
}
|
|
|
|
guard let iconImage = NSImage(contentsOfFile: inputPath) else {
|
|
fputs("Failed to load image: \(inputPath)\n", stderr)
|
|
exit(1)
|
|
}
|
|
|
|
// Get the largest representation for best quality
|
|
guard let rep = iconImage.representations.max(by: { $0.pixelsWide < $1.pixelsWide }) else {
|
|
fputs("No image representations found\n", stderr)
|
|
exit(1)
|
|
}
|
|
|
|
let size = NSSize(width: rep.pixelsWide, height: rep.pixelsHigh)
|
|
|
|
let newImage = NSImage(size: size)
|
|
newImage.lockFocus()
|
|
|
|
// Draw the original icon
|
|
iconImage.draw(in: NSRect(origin: .zero, size: size))
|
|
|
|
// Configure badge
|
|
let singleLineHeight = size.height * 0.22
|
|
let padding = size.width * 0.03
|
|
let maxBadgeWidth = size.width * 0.9
|
|
|
|
// Text attributes - calculate font size to fit
|
|
let maxFontSize = singleLineHeight * 0.65
|
|
var fontSize = maxFontSize
|
|
var attributes: [NSAttributedString.Key: Any]
|
|
|
|
// Helper to wrap text on `-` characters
|
|
func wrapText(_ text: String, maxWidth: CGFloat, attributes: [NSAttributedString.Key: Any]) -> [String] {
|
|
let singleLineSize = (text as NSString).size(withAttributes: attributes)
|
|
if singleLineSize.width <= maxWidth {
|
|
return [text]
|
|
}
|
|
|
|
// Split on `-` and try to form lines
|
|
let parts = text.components(separatedBy: "-")
|
|
if parts.count == 1 {
|
|
return [text] // No `-` to wrap on
|
|
}
|
|
|
|
var lines: [String] = []
|
|
var currentLine = ""
|
|
|
|
for (index, part) in parts.enumerated() {
|
|
let separator = index == 0 ? "" : "-"
|
|
let testLine = currentLine.isEmpty ? part : currentLine + separator + part
|
|
let testSize = (testLine as NSString).size(withAttributes: attributes)
|
|
|
|
if testSize.width <= maxWidth || currentLine.isEmpty {
|
|
currentLine = testLine
|
|
} else {
|
|
lines.append(currentLine)
|
|
currentLine = part
|
|
}
|
|
}
|
|
if !currentLine.isEmpty {
|
|
lines.append(currentLine)
|
|
}
|
|
|
|
return lines
|
|
}
|
|
|
|
// Find font size that fits (allowing up to 2 lines)
|
|
var lines: [String] = []
|
|
repeat {
|
|
attributes = [
|
|
.font: NSFont.systemFont(ofSize: fontSize, weight: .bold),
|
|
.foregroundColor: NSColor.white
|
|
]
|
|
lines = wrapText(label, maxWidth: maxBadgeWidth - padding * 4, attributes: attributes)
|
|
fontSize -= 1
|
|
} while lines.count > 2 && fontSize > 8
|
|
|
|
// Calculate text dimensions using typographic metrics
|
|
let lineHeight = (lines.first! as NSString).size(withAttributes: attributes).height
|
|
let textHeight = lineHeight * CGFloat(lines.count)
|
|
let maxLineWidth = lines.map { ($0 as NSString).size(withAttributes: attributes).width }.max() ?? 0
|
|
|
|
// Badge dimensions based on text
|
|
let badgeHeight = textHeight + padding * 2
|
|
let cornerRadius = badgeHeight * 0.2
|
|
let badgeWidth = maxLineWidth + padding * 4
|
|
let badgeX = (size.width - badgeWidth) / 2
|
|
let badgeY = size.height - badgeHeight - padding - size.height * 0.05
|
|
|
|
// Draw badge background (light blue semi-transparent)
|
|
let badgePath = NSBezierPath(roundedRect: NSRect(x: badgeX, y: badgeY, width: badgeWidth, height: badgeHeight),
|
|
xRadius: cornerRadius, yRadius: cornerRadius)
|
|
NSColor(calibratedRed: 0.4, green: 0.7, blue: 1.0, alpha: 0.85).setFill()
|
|
badgePath.fill()
|
|
|
|
// Draw text centered in badge (multiple lines, bottom to top)
|
|
for (index, line) in lines.reversed().enumerated() {
|
|
let lineSize = (line as NSString).size(withAttributes: attributes)
|
|
let textX = badgeX + (badgeWidth - lineSize.width) / 2
|
|
let textY = badgeY + padding + lineHeight * CGFloat(index)
|
|
(line as NSString).draw(at: NSPoint(x: textX, y: textY), withAttributes: attributes)
|
|
}
|
|
|
|
newImage.unlockFocus()
|
|
|
|
// First, create PNG data at multiple sizes for icns
|
|
guard let tiffData = newImage.tiffRepresentation,
|
|
let bitmapRep = NSBitmapImageRep(data: tiffData) else {
|
|
fputs("Failed to create bitmap representation\n", stderr)
|
|
exit(1)
|
|
}
|
|
|
|
// For simplicity, we'll create a PNG and then use iconutil if available,
|
|
// or just save as PNG for the icon (Tauri can use PNG)
|
|
guard let pngData = bitmapRep.representation(using: .png, properties: [:]) else {
|
|
fputs("Failed to create PNG data\n", stderr)
|
|
exit(1)
|
|
}
|
|
|
|
// If output is .icns, we need to create an iconset and use iconutil
|
|
if outputPath.hasSuffix(".icns") {
|
|
let tempDir = FileManager.default.temporaryDirectory
|
|
let iconsetParent = tempDir.appendingPathComponent(
|
|
"staged-dev-\(sanitizedPathComponent(label))-\(UUID().uuidString)",
|
|
isDirectory: true
|
|
)
|
|
let iconsetPath = iconsetParent.appendingPathComponent("icon.iconset", isDirectory: true)
|
|
try! FileManager.default.createDirectory(at: iconsetPath, withIntermediateDirectories: true)
|
|
|
|
// Generate all required sizes for iconset
|
|
let sizes: [(name: String, size: Int)] = [
|
|
("icon_16x16", 16),
|
|
("icon_16x16@2x", 32),
|
|
("icon_32x32", 32),
|
|
("icon_32x32@2x", 64),
|
|
("icon_128x128", 128),
|
|
("icon_128x128@2x", 256),
|
|
("icon_256x256", 256),
|
|
("icon_256x256@2x", 512),
|
|
("icon_512x512", 512),
|
|
("icon_512x512@2x", 1024)
|
|
]
|
|
|
|
for (name, targetSize) in sizes {
|
|
let resizedImage = NSImage(size: NSSize(width: targetSize, height: targetSize))
|
|
resizedImage.lockFocus()
|
|
NSGraphicsContext.current?.imageInterpolation = .high
|
|
newImage.draw(in: NSRect(x: 0, y: 0, width: targetSize, height: targetSize))
|
|
resizedImage.unlockFocus()
|
|
|
|
guard let resizedTiff = resizedImage.tiffRepresentation,
|
|
let resizedBitmap = NSBitmapImageRep(data: resizedTiff),
|
|
let resizedPng = resizedBitmap.representation(using: .png, properties: [:]) else {
|
|
continue
|
|
}
|
|
|
|
let filePath = iconsetPath.appendingPathComponent("\(name).png")
|
|
try! resizedPng.write(to: filePath)
|
|
}
|
|
|
|
// Use iconutil to create icns
|
|
let process = Process()
|
|
process.executableURL = URL(fileURLWithPath: "/usr/bin/iconutil")
|
|
process.arguments = ["-c", "icns", iconsetPath.path, "-o", outputPath]
|
|
try! process.run()
|
|
process.waitUntilExit()
|
|
|
|
try? FileManager.default.removeItem(at: iconsetParent)
|
|
|
|
if process.terminationStatus != 0 {
|
|
fputs("iconutil failed\n", stderr)
|
|
exit(1)
|
|
}
|
|
} else {
|
|
// Just save as PNG
|
|
try! pngData.write(to: URL(fileURLWithPath: outputPath))
|
|
}
|
|
|
|
print("Generated: \(outputPath)")
|