Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion Sources/Pesty/Util/AppIconProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,41 @@ import AppKit

@MainActor
enum AppIconProvider {
private static let pestyBundleID = "com.greycorelabs.pesty"
private static var cache: [String: NSImage] = [:]

static func icon(forBundleID bundleID: String?) -> NSImage {
guard let bundleID else { return generic }
if let cached = cache[bundleID] { return cached }
var image = generic
if let url = NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundleID) {
if bundleID == pestyBundleID || bundleID == Bundle.main.bundleIdentifier {
image = pestyIcon()
} else if let url = NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundleID) {
image = NSWorkspace.shared.icon(forFile: url.path)
}
cache[bundleID] = image
return image
}

private static func pestyIcon() -> NSImage {
if let url = Bundle.main.url(forResource: "Pesty", withExtension: "icns"),
let icon = NSImage(contentsOf: url) {
return icon
}
let projectRoot = URL(fileURLWithPath: #filePath)
.deletingLastPathComponent()
.deletingLastPathComponent()
.deletingLastPathComponent()
let developmentIcon = projectRoot.appending(path: "packaging/Pesty.icns")
if let icon = NSImage(contentsOf: developmentIcon) {
return icon
}
if let url = NSWorkspace.shared.urlForApplication(withBundleIdentifier: pestyBundleID) {
return NSWorkspace.shared.icon(forFile: url.path)
}
return NSApp.applicationIconImage ?? generic
}

static let generic: NSImage =
NSImage(systemSymbolName: "app.dashed", accessibilityDescription: nil)
?? NSImage()
Expand Down
88 changes: 63 additions & 25 deletions Sources/Pesty/Util/SourceColor.swift
Original file line number Diff line number Diff line change
@@ -1,33 +1,71 @@
import AppKit
import SwiftUI

@MainActor
enum SourceColor {
private static let palette: [Color] = [
Color(red: 0.85, green: 0.66, blue: 0.22),
Color(red: 0.34, green: 0.56, blue: 0.82),
Color(red: 0.72, green: 0.38, blue: 0.58),
Color(red: 0.27, green: 0.62, blue: 0.55),
Color(red: 0.80, green: 0.40, blue: 0.34),
Color(red: 0.45, green: 0.40, blue: 0.74),
Color(red: 0.49, green: 0.62, blue: 0.30),
Color(red: 0.84, green: 0.52, blue: 0.27),
Color(red: 0.30, green: 0.49, blue: 0.74),
Color(red: 0.62, green: 0.42, blue: 0.30),
Color(red: 0.74, green: 0.36, blue: 0.42),
Color(red: 0.40, green: 0.55, blue: 0.62)
]

private static let key = "appColorMap"
private static var map: [String: Int] = {
UserDefaults.standard.dictionary(forKey: key) as? [String: Int] ?? [:]
}()
private static var cache: [String: Color] = [:]
private static let fallback = Color(red: 0.02, green: 0.48, blue: 1.0)

static func color(for bundleID: String?) -> Color {
guard let id = bundleID, !id.isEmpty else { return palette[0] }
if let i = map[id] { return palette[i % palette.count] }
let i = map.count % palette.count
map[id] = i
UserDefaults.standard.set(map, forKey: key)
return palette[i]
guard let id = bundleID, !id.isEmpty else { return fallback }
if let color = cache[id] { return color }
let color = dominantColor(in: AppIconProvider.icon(forBundleID: id)) ?? fallback
cache[id] = color
return color
}

private static func dominantColor(in icon: NSImage) -> Color? {
let size = 40
let thumbnail = NSImage(size: NSSize(width: size, height: size))
thumbnail.lockFocus()
NSGraphicsContext.current?.imageInterpolation = .high
icon.draw(in: NSRect(x: 0, y: 0, width: size, height: size),
from: .zero,
operation: .sourceOver,
fraction: 1,
respectFlipped: true,
hints: [.interpolation: NSImageInterpolation.high])
thumbnail.unlockFocus()
guard let data = thumbnail.tiffRepresentation,
let bitmap = NSBitmapImageRep(data: data) else { return nil }

var red = 0.0
var green = 0.0
var blue = 0.0
var weight = 0.0
var darkWeight = 0.0

for x in 0..<size {
for y in 0..<size {
guard let color = bitmap.colorAt(x: x, y: y)?.usingColorSpace(.deviceRGB) else { continue }
let alpha = color.alphaComponent
guard alpha > 0.35 else { continue }
let maximum = max(color.redComponent, color.greenComponent, color.blueComponent)
let minimum = min(color.redComponent, color.greenComponent, color.blueComponent)
let saturation = maximum == 0 ? 0 : (maximum - minimum) / maximum
let brightness = maximum

if brightness < 0.45 { darkWeight += alpha }
guard saturation > 0.16, brightness > 0.14 else { continue }
let pixelWeight = alpha * saturation * (0.45 + 0.55 * brightness)
red += Double(color.redComponent) * pixelWeight
green += Double(color.greenComponent) * pixelWeight
blue += Double(color.blueComponent) * pixelWeight
weight += pixelWeight
}
}

if weight == 0 {
return darkWeight > 0 ? Color(red: 0.025, green: 0.075, blue: 0.24) : fallback
}

let main = NSColor(deviceRed: red / weight, green: green / weight, blue: blue / weight, alpha: 1)
var hue: CGFloat = 0
var saturation: CGFloat = 0
var brightness: CGFloat = 0
main.getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: nil)
return Color(hue: Double(hue),
saturation: min(0.99, max(0.90, Double(saturation) * 1.85)),
brightness: min(0.98, max(0.84, Double(brightness) * 1.20)))
}
}