diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index 9bd36eaad..0b3432362 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -72,6 +72,7 @@ Features/About/About.xib, Features/About/AboutController.swift, Features/About/AboutView.swift, + Features/About/CyclingIconView.swift, "Features/App Intents/CloseTerminalIntent.swift", "Features/App Intents/CommandPaletteIntent.swift", "Features/App Intents/Entities/CommandEntity.swift", diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index 2e62d033b..e8c56e9d9 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -946,33 +946,8 @@ class AppDelegate: NSObject, var appIconName: String? = config.macosIcon.rawValue switch (config.macosIcon) { - case .official: - // Discard saved icon name - appIconName = nil - break - case .blueprint: - appIcon = NSImage(named: "BlueprintImage")! - - case .chalkboard: - appIcon = NSImage(named: "ChalkboardImage")! - - case .glass: - appIcon = NSImage(named: "GlassImage")! - - case .holographic: - appIcon = NSImage(named: "HolographicImage")! - - case .microchip: - appIcon = NSImage(named: "MicrochipImage")! - - case .paper: - appIcon = NSImage(named: "PaperImage")! - - case .retro: - appIcon = NSImage(named: "RetroImage")! - - case .xray: - appIcon = NSImage(named: "XrayImage")! + case let icon where icon.assetName != nil: + appIcon = NSImage(named: icon.assetName!)! case .custom: if let userIcon = NSImage(contentsOfFile: config.macosCustomIcon) { @@ -982,6 +957,7 @@ class AppDelegate: NSObject, appIcon = nil // Revert back to official icon if invalid location appIconName = nil // Discard saved icon name } + case .customStyle: // Discard saved icon name // if no valid colours were found @@ -997,6 +973,10 @@ class AppDelegate: NSObject, let colorStrings = ([ghostColor] + screenColors).compactMap(\.hexString) appIconName = (colorStrings + [config.macosIconFrame.rawValue]) .joined(separator: "_") + + default: + // Discard saved icon name + appIconName = nil } // Only change the icon if it has actually changed from the current one, diff --git a/macos/Sources/Features/About/AboutView.swift b/macos/Sources/Features/About/AboutView.swift index 6ed3285ed..967eb16b0 100644 --- a/macos/Sources/Features/About/AboutView.swift +++ b/macos/Sources/Features/About/AboutView.swift @@ -44,10 +44,7 @@ struct AboutView: View { var body: some View { VStack(alignment: .center) { - ghosttyIconImage() - .resizable() - .aspectRatio(contentMode: .fit) - .frame(height: 128) + CyclingIconView() VStack(alignment: .center, spacing: 32) { VStack(alignment: .center, spacing: 8) { diff --git a/macos/Sources/Features/About/CyclingIconView.swift b/macos/Sources/Features/About/CyclingIconView.swift new file mode 100644 index 000000000..4274278e0 --- /dev/null +++ b/macos/Sources/Features/About/CyclingIconView.swift @@ -0,0 +1,62 @@ +import SwiftUI +import GhosttyKit + +/// A view that cycles through Ghostty's official icon variants. +struct CyclingIconView: View { + @State private var currentIcon: Ghostty.MacOSIcon = .official + @State private var isHovering: Bool = false + + private let icons: [Ghostty.MacOSIcon] = [ + .official, + .blueprint, + .chalkboard, + .microchip, + .glass, + .holographic, + .paper, + .retro, + .xray, + ] + private let timerPublisher = Timer.publish(every: 3, on: .main, in: .common) + + var body: some View { + ZStack { + iconView(for: currentIcon) + .id(currentIcon) + } + .animation(.easeInOut(duration: 0.5), value: currentIcon) + .frame(height: 128) + .onReceive(timerPublisher.autoconnect()) { _ in + if !isHovering { + advanceToNextIcon() + } + } + .onHover { hovering in + isHovering = hovering + } + .onTapGesture { + advanceToNextIcon() + } + .help("macos-icon = \(currentIcon.rawValue)") + .accessibilityLabel("Ghostty Application Icon") + .accessibilityHint("Click to cycle through icon variants") + } + + @ViewBuilder + private func iconView(for icon: Ghostty.MacOSIcon) -> some View { + let iconImage: Image = switch icon.assetName { + case let assetName?: Image(assetName) + case nil: ghosttyIconImage() + } + + iconImage + .resizable() + .aspectRatio(contentMode: .fit) + } + + private func advanceToNextIcon() { + let currentIndex = icons.firstIndex(of: currentIcon) ?? 0 + let nextIndex = icons.indexWrapping(after: currentIndex) + currentIcon = icons[nextIndex] + } +} diff --git a/macos/Sources/Ghostty/Package.swift b/macos/Sources/Ghostty/Package.swift index aa62c16f7..15cb3a51e 100644 --- a/macos/Sources/Ghostty/Package.swift +++ b/macos/Sources/Ghostty/Package.swift @@ -330,6 +330,22 @@ extension Ghostty { case xray case custom case customStyle = "custom-style" + + /// Bundled asset name for built-in icons + var assetName: String? { + switch self { + case .official: return nil + case .blueprint: return "BlueprintImage" + case .chalkboard: return "ChalkboardImage" + case .microchip: return "MicrochipImage" + case .glass: return "GlassImage" + case .holographic: return "HolographicImage" + case .paper: return "PaperImage" + case .retro: return "RetroImage" + case .xray: return "XrayImage" + case .custom, .customStyle: return nil + } + } } /// macos-icon-frame