From f831f68f1aab3148e8d46362cb9991425e62f395 Mon Sep 17 00:00:00 2001 From: Lukas <134181853+bo2themax@users.noreply.github.com> Date: Tue, 24 Feb 2026 09:51:30 +0100 Subject: [PATCH] macOS: update AppIcon encoding - make `ColorizedGhosttyIcon` codable - remove deprecated string encoding introduced in tip --- .../Features/Custom App Icon/AppIcon.swift | 78 +++---------------- .../ColorizedGhosttyIcon.swift | 35 ++++++++- .../Extensions/UserDefaults+AppIcon.swift | 8 -- 3 files changed, 44 insertions(+), 77 deletions(-) diff --git a/macos/Sources/Features/Custom App Icon/AppIcon.swift b/macos/Sources/Features/Custom App Icon/AppIcon.swift index 52ab9ef95..296bd10fe 100644 --- a/macos/Sources/Features/Custom App Icon/AppIcon.swift +++ b/macos/Sources/Features/Custom App Icon/AppIcon.swift @@ -13,9 +13,9 @@ enum AppIcon: Equatable, Codable { case retro case xray /// Save full image data to avoid sandboxing issues - case custom(fileData: Data) - case customStyle(ghostColorHex: String, screenColorHexes: [String], iconFrame: Ghostty.MacOSIconFrame) - + case custom(_ iconFile: Data) + case customStyle(_ icon: ColorizedGhosttyIcon) + #if !DOCK_TILE_PLUGIN init?(config: Ghostty.Config) { switch config.macosIcon { @@ -39,7 +39,7 @@ enum AppIcon: Equatable, Codable { self = .xray case .custom: if let data = try? Data(contentsOf: URL(filePath: config.macosCustomIcon, relativeTo: nil)) { - self = .custom(fileData: data) + self = .custom(data) } else { return nil } @@ -47,59 +47,16 @@ enum AppIcon: Equatable, Codable { // Discard saved icon name // if no valid colours were found guard - let ghostColor = config.macosIconGhostColor?.hexString, - let screenColors = config.macosIconScreenColor?.compactMap(\.hexString) + let ghostColor = config.macosIconGhostColor, + let screenColors = config.macosIconScreenColor else { return nil } - self = .customStyle(ghostColorHex: ghostColor, screenColorHexes: screenColors, iconFrame: config.macosIconFrame) + self = .customStyle(ColorizedGhosttyIcon(screenColors: screenColors, ghostColor: ghostColor, frame: config.macosIconFrame)) } } #endif - /// Restore the icon from previously saved values - init?(string: String) { - switch string { - case Ghostty.MacOSIcon.official.rawValue: - self = .official - case Ghostty.MacOSIcon.blueprint.rawValue: - self = .blueprint - case Ghostty.MacOSIcon.chalkboard.rawValue: - self = .chalkboard - case Ghostty.MacOSIcon.glass.rawValue: - self = .glass - case Ghostty.MacOSIcon.holographic.rawValue: - self = .holographic - case Ghostty.MacOSIcon.microchip.rawValue: - self = .microchip - case Ghostty.MacOSIcon.paper.rawValue: - self = .paper - case Ghostty.MacOSIcon.retro.rawValue: - self = .retro - case Ghostty.MacOSIcon.xray.rawValue: - self = .xray - default: - var parts = string.split(separator: "_").map(String.init) - if - let _ = parts.first.flatMap(NSColor.init(hex:)), - let frame = parts.last.flatMap(Ghostty.MacOSIconFrame.init(rawValue:)) - { - let ghostC = parts.removeFirst() - _ = parts.removeLast() - self = .customStyle( - ghostColorHex: ghostC, - screenColorHexes: parts, - iconFrame: frame - ) - } else { - // Due to sandboxing with `com.apple.dock.external.extra.arm64`, - // we can’t restore custom icon file automatically. - // The user must open the app to update it. - return nil - } - } - } - func image(in bundle: Bundle) -> NSImage? { switch self { case .official: @@ -121,24 +78,9 @@ enum AppIcon: Equatable, Codable { case .xray: return bundle.image(forResource: "XrayImage")! case let .custom(file): - if let userIcon = NSImage(data: file) { - return userIcon - } else { - return nil - } - case let .customStyle(ghostColorHex, screenColorHexes, macosIconFrame): - let screenColors = screenColorHexes.compactMap(NSColor.init(hex:)) - guard - let ghostColor = NSColor(hex: ghostColorHex), - let icon = ColorizedGhosttyIcon( - screenColors: screenColors, - ghostColor: ghostColor, - frame: macosIconFrame - ).makeImage(in: bundle) - else { - return nil - } - return icon + return NSImage(data: file) + case let .customStyle(customIcon): + return customIcon.makeImage(in: bundle) } } } diff --git a/macos/Sources/Features/Custom App Icon/ColorizedGhosttyIcon.swift b/macos/Sources/Features/Custom App Icon/ColorizedGhosttyIcon.swift index df24477d4..62f58a063 100644 --- a/macos/Sources/Features/Custom App Icon/ColorizedGhosttyIcon.swift +++ b/macos/Sources/Features/Custom App Icon/ColorizedGhosttyIcon.swift @@ -1,6 +1,33 @@ import Cocoa -struct ColorizedGhosttyIcon { +struct ColorizedGhosttyIcon: Codable, Equatable { + init(screenColors: [NSColor], ghostColor: NSColor, frame: Ghostty.MacOSIconFrame) { + self.screenColors = screenColors + self.ghostColor = ghostColor + self.frame = frame + } + + init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let screenColorHexes = try container.decode([String].self, forKey: .screenColors) + let screenColors = screenColorHexes.compactMap(NSColor.init(hex:)) + let ghostColorHex = try container.decode(String.self, forKey: .ghostColor) + guard let ghostColor = NSColor(hex: ghostColorHex) else { + throw NSError(domain: "Custom Icon Error", code: 1, userInfo: [ + NSLocalizedDescriptionKey: "Failed to decode ghost color from \(ghostColorHex)" + ]) + } + let frame = try container.decode(Ghostty.MacOSIconFrame.self, forKey: .frame) + self.init(screenColors: screenColors, ghostColor: ghostColor, frame: frame) + } + + func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(screenColors.compactMap(\.hexString), forKey: .screenColors) + try container.encode(ghostColor.hexString, forKey: .ghostColor) + try container.encode(frame, forKey: .frame) + } + /// The colors that make up the gradient of the screen. let screenColors: [NSColor] @@ -10,6 +37,12 @@ struct ColorizedGhosttyIcon { /// The frame type to use let frame: Ghostty.MacOSIconFrame + private enum CodingKeys: String, CodingKey { + case screenColors + case ghostColor + case frame + } + /// Make a custom colorized ghostty icon. func makeImage(in bundle: Bundle) -> NSImage? { // All of our layers (not in order) diff --git a/macos/Sources/Features/Custom App Icon/Extensions/UserDefaults+AppIcon.swift b/macos/Sources/Features/Custom App Icon/Extensions/UserDefaults+AppIcon.swift index cce8e24a4..9478cc5c3 100644 --- a/macos/Sources/Features/Custom App Icon/Extensions/UserDefaults+AppIcon.swift +++ b/macos/Sources/Features/Custom App Icon/Extensions/UserDefaults+AppIcon.swift @@ -11,14 +11,6 @@ extension UserDefaults { removeObject(forKey: Self.customIconKeyOld) } - // If we have an old, pre-docktileplugin value, then we parse the - // the old value (try) and set it. - if let previous = string(forKey: Self.customIconKeyOld), let newIcon = AppIcon(string: previous) { - // update new storage once - self.appIcon = newIcon - return newIcon - } - // Check if we have the new key for our dock tile plugin format. guard let data = data(forKey: Self.customIconKeyNew) else { return nil