macos: cycle through our icons in the About view (#10298)

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`).

---


https://github.com/user-attachments/assets/5cfbd225-bbdf-4077-96c1-7da315ce02cc
This commit is contained in:
Jon Parise
2026-01-14 07:53:51 -05:00
committed by GitHub
5 changed files with 87 additions and 31 deletions

View File

@@ -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",

View File

@@ -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,

View File

@@ -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) {

View File

@@ -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]
}
}

View File

@@ -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