macos: colorized Ghostty icon begins!

This commit is contained in:
Mitchell Hashimoto
2024-12-13 16:08:47 -08:00
parent e97ebc5f7d
commit c4bec781b0
18 changed files with 270 additions and 0 deletions

View File

@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "base.png",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "original"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

View File

@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "crt-effect.png",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "original"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

View File

@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "ghosty.png",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

View File

@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "gloss.png",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "original"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "screen-dark.png",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "original"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

View File

@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "screen-mask.png",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "original"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

@ -39,6 +39,9 @@
A53D0C952B53B4D800305CE6 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A5B30538299BEAAB0047F10C /* Assets.xcassets */; }; A53D0C952B53B4D800305CE6 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A5B30538299BEAAB0047F10C /* Assets.xcassets */; };
A53D0C9B2B543F3B00305CE6 /* Ghostty.App.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53D0C992B543F3B00305CE6 /* Ghostty.App.swift */; }; A53D0C9B2B543F3B00305CE6 /* Ghostty.App.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53D0C992B543F3B00305CE6 /* Ghostty.App.swift */; };
A53D0C9C2B543F7B00305CE6 /* Package.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55B7BB729B6F53A0055DE60 /* Package.swift */; }; A53D0C9C2B543F7B00305CE6 /* Package.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55B7BB729B6F53A0055DE60 /* Package.swift */; };
A54B0CE92D0CECD100CBEFF8 /* ColorizedGhosttyIconView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A54B0CE82D0CECD100CBEFF8 /* ColorizedGhosttyIconView.swift */; };
A54B0CEB2D0CFB4C00CBEFF8 /* NSImage+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A54B0CEA2D0CFB4A00CBEFF8 /* NSImage+Extension.swift */; };
A54B0CED2D0CFB7700CBEFF8 /* ColorizedGhosttyIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = A54B0CEC2D0CFB7300CBEFF8 /* ColorizedGhosttyIcon.swift */; };
A54D786C2CA7978E001B19B1 /* BaseTerminalController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A54D786B2CA79788001B19B1 /* BaseTerminalController.swift */; }; A54D786C2CA7978E001B19B1 /* BaseTerminalController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A54D786B2CA79788001B19B1 /* BaseTerminalController.swift */; };
A55685E029A03A9F004303CE /* AppError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55685DF29A03A9F004303CE /* AppError.swift */; }; A55685E029A03A9F004303CE /* AppError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55685DF29A03A9F004303CE /* AppError.swift */; };
A55B7BB829B6F53A0055DE60 /* Package.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55B7BB729B6F53A0055DE60 /* Package.swift */; }; A55B7BB829B6F53A0055DE60 /* Package.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55B7BB729B6F53A0055DE60 /* Package.swift */; };
@ -124,6 +127,9 @@
A53A6C022CCC1B7D00943E98 /* Ghostty.Action.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.Action.swift; sourceTree = "<group>"; }; A53A6C022CCC1B7D00943E98 /* Ghostty.Action.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.Action.swift; sourceTree = "<group>"; };
A53D0C932B53B43700305CE6 /* iOSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSApp.swift; sourceTree = "<group>"; }; A53D0C932B53B43700305CE6 /* iOSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSApp.swift; sourceTree = "<group>"; };
A53D0C992B543F3B00305CE6 /* Ghostty.App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.App.swift; sourceTree = "<group>"; }; A53D0C992B543F3B00305CE6 /* Ghostty.App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.App.swift; sourceTree = "<group>"; };
A54B0CE82D0CECD100CBEFF8 /* ColorizedGhosttyIconView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorizedGhosttyIconView.swift; sourceTree = "<group>"; };
A54B0CEA2D0CFB4A00CBEFF8 /* NSImage+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSImage+Extension.swift"; sourceTree = "<group>"; };
A54B0CEC2D0CFB7300CBEFF8 /* ColorizedGhosttyIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorizedGhosttyIcon.swift; sourceTree = "<group>"; };
A54D786B2CA79788001B19B1 /* BaseTerminalController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseTerminalController.swift; sourceTree = "<group>"; }; A54D786B2CA79788001B19B1 /* BaseTerminalController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseTerminalController.swift; sourceTree = "<group>"; };
A55685DF29A03A9F004303CE /* AppError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppError.swift; sourceTree = "<group>"; }; A55685DF29A03A9F004303CE /* AppError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppError.swift; sourceTree = "<group>"; };
A55B7BB729B6F53A0055DE60 /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = "<group>"; }; A55B7BB729B6F53A0055DE60 /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = "<group>"; };
@ -237,6 +243,7 @@
A57D79252C9C8782001D522E /* Secure Input */, A57D79252C9C8782001D522E /* Secure Input */,
A534263E2A7DCC5800EBB7A2 /* Settings */, A534263E2A7DCC5800EBB7A2 /* Settings */,
A51BFC1C2B2FB5AB00E92F16 /* About */, A51BFC1C2B2FB5AB00E92F16 /* About */,
A54B0CE72D0CEC9800CBEFF8 /* Colorized Ghostty Icon */,
A51BFC292B30F69F00E92F16 /* Update */, A51BFC292B30F69F00E92F16 /* Update */,
); );
path = Features; path = Features;
@ -256,6 +263,7 @@
A5CBD0552C9E65A50017A1AE /* DraggableWindowView.swift */, A5CBD0552C9E65A50017A1AE /* DraggableWindowView.swift */,
C159E81C2B66A06B00FDFE9C /* OSColor+Extension.swift */, C159E81C2B66A06B00FDFE9C /* OSColor+Extension.swift */,
A599CDAF2CF103F20049FA26 /* NSAppearance+Extension.swift */, A599CDAF2CF103F20049FA26 /* NSAppearance+Extension.swift */,
A54B0CEA2D0CFB4A00CBEFF8 /* NSImage+Extension.swift */,
A52FFF5C2CAB4D05000C6A5B /* NSScreen+Extension.swift */, A52FFF5C2CAB4D05000C6A5B /* NSScreen+Extension.swift */,
C1F26EA62B738B9900404083 /* NSView+Extension.swift */, C1F26EA62B738B9900404083 /* NSView+Extension.swift */,
AEE8B3442B9AA39600260C5E /* NSPasteboard+Extension.swift */, AEE8B3442B9AA39600260C5E /* NSPasteboard+Extension.swift */,
@ -307,6 +315,15 @@
path = macOS; path = macOS;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
A54B0CE72D0CEC9800CBEFF8 /* Colorized Ghostty Icon */ = {
isa = PBXGroup;
children = (
A54B0CEC2D0CFB7300CBEFF8 /* ColorizedGhosttyIcon.swift */,
A54B0CE82D0CECD100CBEFF8 /* ColorizedGhosttyIconView.swift */,
);
path = "Colorized Ghostty Icon";
sourceTree = "<group>";
};
A54CD6ED299BEB14008C95BB /* Sources */ = { A54CD6ED299BEB14008C95BB /* Sources */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -580,8 +597,10 @@
files = ( files = (
A59630A42AF059BB00D64628 /* Ghostty.SplitNode.swift in Sources */, A59630A42AF059BB00D64628 /* Ghostty.SplitNode.swift in Sources */,
A514C8D62B54A16400493A16 /* Ghostty.Config.swift in Sources */, A514C8D62B54A16400493A16 /* Ghostty.Config.swift in Sources */,
A54B0CEB2D0CFB4C00CBEFF8 /* NSImage+Extension.swift in Sources */,
A54D786C2CA7978E001B19B1 /* BaseTerminalController.swift in Sources */, A54D786C2CA7978E001B19B1 /* BaseTerminalController.swift in Sources */,
A59FB5CF2AE0DB50009128F3 /* InspectorView.swift in Sources */, A59FB5CF2AE0DB50009128F3 /* InspectorView.swift in Sources */,
A54B0CE92D0CECD100CBEFF8 /* ColorizedGhosttyIconView.swift in Sources */,
A5D0AF3D2B37804400D21823 /* CodableBridge.swift in Sources */, A5D0AF3D2B37804400D21823 /* CodableBridge.swift in Sources */,
A5D0AF3B2B36A1DE00D21823 /* TerminalRestorable.swift in Sources */, A5D0AF3B2B36A1DE00D21823 /* TerminalRestorable.swift in Sources */,
C1F26EA72B738B9900404083 /* NSView+Extension.swift in Sources */, C1F26EA72B738B9900404083 /* NSView+Extension.swift in Sources */,
@ -620,6 +639,7 @@
A5CEAFDC29B8009000646FDA /* SplitView.swift in Sources */, A5CEAFDC29B8009000646FDA /* SplitView.swift in Sources */,
A5CDF1932AAF9E0800513312 /* ConfigurationErrorsController.swift in Sources */, A5CDF1932AAF9E0800513312 /* ConfigurationErrorsController.swift in Sources */,
A53A6C032CCC1B7F00943E98 /* Ghostty.Action.swift in Sources */, A53A6C032CCC1B7F00943E98 /* Ghostty.Action.swift in Sources */,
A54B0CED2D0CFB7700CBEFF8 /* ColorizedGhosttyIcon.swift in Sources */,
A59FB5D12AE0DEA7009128F3 /* MetalView.swift in Sources */, A59FB5D12AE0DEA7009128F3 /* MetalView.swift in Sources */,
A55685E029A03A9F004303CE /* AppError.swift in Sources */, A55685E029A03A9F004303CE /* AppError.swift in Sources */,
A599CDB02CF103F60049FA26 /* NSAppearance+Extension.swift in Sources */, A599CDB02CF103F60049FA26 /* NSAppearance+Extension.swift in Sources */,

View File

@ -519,6 +519,13 @@ class AppDelegate: NSObject,
} else { } else {
GlobalEventTap.shared.disable() GlobalEventTap.shared.disable()
} }
if let colorizedIcon = ColorizedGhosttyIcon(
screenColors: [.purple, .blue],
ghostColor: .yellow
).makeImage() {
NSApplication.shared.applicationIconImage = colorizedIcon
}
} }
/// Sync the appearance of our app with the theme specified in the config. /// Sync the appearance of our app with the theme specified in the config.

View File

@ -0,0 +1,45 @@
import Cocoa
struct ColorizedGhosttyIcon {
/// The colors that make up the gradient of the screen.
let screenColors: [NSColor]
/// The color of the ghost.
let ghostColor: NSColor
/// Make a custom colorized ghostty icon.
func makeImage() -> NSImage? {
// All of our layers (in order)
guard let base = NSImage(named: "CustomIconBase") else { return nil }
guard let screen = NSImage(named: "CustomIconScreen") else { return nil }
guard let screenMask = NSImage(named: "CustomIconScreenMask") else { return nil }
guard let ghost = NSImage(named: "CustomIconGhost") else { return nil }
guard let crt = NSImage(named: "CustomIconCRT") else { return nil }
guard let gloss = NSImage(named: "CustomIconGloss") else { return nil }
// Apply our color in various ways to our layers.
// NOTE: These functions are not built-in, they're implemented as an extension
// to NSImage in NSImage+Extension.swift.
guard let screenGradient = screenMask.gradient(colors: screenColors) else { return nil }
guard let tintedGhost = ghost.tint(color: ghostColor) else { return nil }
// Combine our layers using the proper blending modes
return.combine(images: [
base,
screen,
screenGradient,
ghost,
tintedGhost,
crt,
gloss,
], blendingModes: [
.normal,
.normal,
.color,
.normal,
.color,
.overlay,
.normal,
])
}
}

View File

@ -0,0 +1,12 @@
import SwiftUI
import Cocoa
// For testing.
struct ColorizedGhosttyIconView: View {
var body: some View {
Image(nsImage: ColorizedGhosttyIcon(
screenColors: [.purple, .blue],
ghostColor: .yellow
).makeImage()!)
}
}

View File

@ -0,0 +1,90 @@
import Cocoa
extension NSImage {
/// Combine multiple images with the given blend modes. This is useful given a set
/// of layers to create a final rasterized image.
static func combine(images: [NSImage], blendingModes: [CGBlendMode]) -> NSImage? {
guard images.count == blendingModes.count else { return nil }
guard images.count > 0 else { return nil }
// The final size will be the same size as our first image.
let size = images.first!.size
// Create a bitmap context manually
guard let bitmapContext = CGContext(
data: nil,
width: Int(size.width),
height: Int(size.height),
bitsPerComponent: 8,
bytesPerRow: 0,
space: CGColorSpaceCreateDeviceRGB(),
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue
) else { return nil }
// Clear the context
bitmapContext.setFillColor(.clear)
bitmapContext.fill(.init(origin: .zero, size: size))
// Draw each image with its corresponding blend mode
for (index, image) in images.enumerated() {
guard let cgImage = image.cgImage(
forProposedRect: nil,
context: nil,
hints: nil
) else { return nil }
let blendMode = blendingModes[index]
bitmapContext.setBlendMode(blendMode)
bitmapContext.draw(cgImage, in: CGRect(origin: .zero, size: size))
}
// Create a CGImage from the context
guard let combinedCGImage = bitmapContext.makeImage() else { return nil }
// Wrap the CGImage in an NSImage
return NSImage(cgImage: combinedCGImage, size: size)
}
/// Apply a gradient onto this image, using this image as a mask.
func gradient(colors: [NSColor]) -> NSImage? {
let resultImage = NSImage(size: size)
resultImage.lockFocus()
defer { resultImage.unlockFocus() }
// Draw the gradient
guard let gradient = NSGradient(colors: colors) else { return nil }
gradient.draw(in: .init(origin: .zero, size: size), angle: 90)
// Apply the mask
draw(at: .zero, from: .zero, operation: .destinationIn, fraction: 1.0)
return resultImage
}
// Tint an NSImage with the given color by applying a basic fill on top of it.
func tint(color: NSColor) -> NSImage? {
// Create a new image with the same size as the base image
let newImage = NSImage(size: size)
// Draw into the new image
newImage.lockFocus()
defer { newImage.unlockFocus() }
// Set up the drawing context
guard let context = NSGraphicsContext.current?.cgContext else { return nil }
defer { context.restoreGState() }
// Draw the base image
guard let cgImage = cgImage(forProposedRect: nil, context: nil, hints: nil) else { return nil }
context.draw(cgImage, in: .init(origin: .zero, size: size))
// Set the tint color and blend mode
context.setFillColor(color.cgColor)
context.setBlendMode(.sourceAtop)
// Apply the tint color over the entire image
context.fill(.init(origin: .zero, size: size))
return newImage
}
}