From 1537590a5f25bca16f2516efda33a5571f100008 Mon Sep 17 00:00:00 2001 From: Jon Parise Date: Mon, 12 Jan 2026 11:40:22 -0500 Subject: [PATCH] macos: cycle through our icons in the About view Clicking on the icon immediately advances to the next one. Hovering on the icon pauses the automatic cycling, and the "help" tooltip displays the icon's configuration name (for `macos-icon`). --- macos/Ghostty.xcodeproj/project.pbxproj | 1 + macos/Sources/App/macOS/AppDelegate.swift | 34 +++------- macos/Sources/Features/About/AboutView.swift | 5 +- .../Features/About/CyclingIconView.swift | 62 +++++++++++++++++++ macos/Sources/Ghostty/Package.swift | 16 +++++ 5 files changed, 87 insertions(+), 31 deletions(-) create mode 100644 macos/Sources/Features/About/CyclingIconView.swift 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