macOS: Customize Ghostty Icon (#3063)
This is some last minute flare to get into the 1.0 release. 😄 This introduces some new configurations to let you customize the macOS app icon at runtime. The runtime icon only applies to certain areas such as the dock, cmd-tab, etc. It does not change the icon in Finder or on disk since Apple requires those icons are bundled as files with the signed app bundle (i.e. you can't change it later without resigning). I still think this introduces a lot of fun into the macOS app. 😄 I'm still finalizing the exact customization options that will exist for the icon, so I documented this option as experimental. I'm feeling pretty good about what's there but I may add other options in the future. ## Demo ### Beige Yellow ``` macos-icon = custom-style macos-icon-frame = beige macos-icon-ghost-color = yellow macos-icon-screen-color = dark goldenrod,dark khaki ```  ### Laker Nation ``` macos-icon = custom-style macos-icon-ghost-color = yellow macos-icon-screen-color = purple,maroon ``` 
@ -333,6 +333,21 @@ typedef struct {
|
||||
uint32_t cell_height_px;
|
||||
} ghostty_surface_size_s;
|
||||
|
||||
// Config types
|
||||
|
||||
// config.Color
|
||||
typedef struct {
|
||||
uint8_t r;
|
||||
uint8_t g;
|
||||
uint8_t b;
|
||||
} ghostty_config_color_s;
|
||||
|
||||
// config.ColorList
|
||||
typedef struct {
|
||||
const ghostty_config_color_s* colors;
|
||||
size_t len;
|
||||
} ghostty_config_color_list_s;
|
||||
|
||||
// apprt.Target.Key
|
||||
typedef enum {
|
||||
GHOSTTY_TARGET_APP,
|
||||
|
6
macos/Assets.xcassets/Custom Icon/Contents.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
15
macos/Assets.xcassets/Custom Icon/CustomIconBaseAluminum.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "base.png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "original"
|
||||
}
|
||||
}
|
BIN
macos/Assets.xcassets/Custom Icon/CustomIconBaseAluminum.imageset/base.png
vendored
Normal file
After Width: | Height: | Size: 145 KiB |
15
macos/Assets.xcassets/Custom Icon/CustomIconBaseBeige.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "beige.png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "original"
|
||||
}
|
||||
}
|
BIN
macos/Assets.xcassets/Custom Icon/CustomIconBaseBeige.imageset/beige.png
vendored
Normal file
After Width: | Height: | Size: 349 KiB |
15
macos/Assets.xcassets/Custom Icon/CustomIconBaseChrome.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "chrome.png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "original"
|
||||
}
|
||||
}
|
BIN
macos/Assets.xcassets/Custom Icon/CustomIconBaseChrome.imageset/chrome.png
vendored
Normal file
After Width: | Height: | Size: 124 KiB |
15
macos/Assets.xcassets/Custom Icon/CustomIconBasePlastic.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "plastic.png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "original"
|
||||
}
|
||||
}
|
BIN
macos/Assets.xcassets/Custom Icon/CustomIconBasePlastic.imageset/plastic.png
vendored
Normal file
After Width: | Height: | Size: 97 KiB |
15
macos/Assets.xcassets/Custom Icon/CustomIconCRT.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "crt-effect.png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "original"
|
||||
}
|
||||
}
|
BIN
macos/Assets.xcassets/Custom Icon/CustomIconCRT.imageset/crt-effect.png
vendored
Normal file
After Width: | Height: | Size: 85 KiB |
15
macos/Assets.xcassets/Custom Icon/CustomIconGhost.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "ghosty.png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
BIN
macos/Assets.xcassets/Custom Icon/CustomIconGhost.imageset/ghosty.png
vendored
Normal file
After Width: | Height: | Size: 63 KiB |
15
macos/Assets.xcassets/Custom Icon/CustomIconGloss.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "gloss.png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "original"
|
||||
}
|
||||
}
|
BIN
macos/Assets.xcassets/Custom Icon/CustomIconGloss.imageset/gloss.png
vendored
Normal file
After Width: | Height: | Size: 3.4 KiB |
15
macos/Assets.xcassets/Custom Icon/CustomIconScreen.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "screen-dark.png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "original"
|
||||
}
|
||||
}
|
BIN
macos/Assets.xcassets/Custom Icon/CustomIconScreen.imageset/screen-dark.png
vendored
Normal file
After Width: | Height: | Size: 8.2 KiB |
15
macos/Assets.xcassets/Custom Icon/CustomIconScreenMask.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "screen-mask.png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "original"
|
||||
}
|
||||
}
|
BIN
macos/Assets.xcassets/Custom Icon/CustomIconScreenMask.imageset/screen-mask.png
vendored
Normal file
After Width: | Height: | Size: 4.3 KiB |
@ -39,6 +39,10 @@
|
||||
A53D0C952B53B4D800305CE6 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A5B30538299BEAAB0047F10C /* Assets.xcassets */; };
|
||||
A53D0C9B2B543F3B00305CE6 /* Ghostty.App.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53D0C992B543F3B00305CE6 /* Ghostty.App.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 */; };
|
||||
A54B0CEF2D0D2E2800CBEFF8 /* ColorizedGhosttyIconImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = A54B0CEE2D0D2E2400CBEFF8 /* ColorizedGhosttyIconImage.swift */; };
|
||||
A54D786C2CA7978E001B19B1 /* BaseTerminalController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A54D786B2CA79788001B19B1 /* BaseTerminalController.swift */; };
|
||||
A55685E029A03A9F004303CE /* AppError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55685DF29A03A9F004303CE /* AppError.swift */; };
|
||||
A55B7BB829B6F53A0055DE60 /* Package.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55B7BB729B6F53A0055DE60 /* Package.swift */; };
|
||||
@ -124,6 +128,10 @@
|
||||
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>"; };
|
||||
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>"; };
|
||||
A54B0CEE2D0D2E2400CBEFF8 /* ColorizedGhosttyIconImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorizedGhosttyIconImage.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>"; };
|
||||
A55B7BB729B6F53A0055DE60 /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = "<group>"; };
|
||||
@ -237,6 +245,7 @@
|
||||
A57D79252C9C8782001D522E /* Secure Input */,
|
||||
A534263E2A7DCC5800EBB7A2 /* Settings */,
|
||||
A51BFC1C2B2FB5AB00E92F16 /* About */,
|
||||
A54B0CE72D0CEC9800CBEFF8 /* Colorized Ghostty Icon */,
|
||||
A51BFC292B30F69F00E92F16 /* Update */,
|
||||
);
|
||||
path = Features;
|
||||
@ -256,6 +265,7 @@
|
||||
A5CBD0552C9E65A50017A1AE /* DraggableWindowView.swift */,
|
||||
C159E81C2B66A06B00FDFE9C /* OSColor+Extension.swift */,
|
||||
A599CDAF2CF103F20049FA26 /* NSAppearance+Extension.swift */,
|
||||
A54B0CEA2D0CFB4A00CBEFF8 /* NSImage+Extension.swift */,
|
||||
A52FFF5C2CAB4D05000C6A5B /* NSScreen+Extension.swift */,
|
||||
C1F26EA62B738B9900404083 /* NSView+Extension.swift */,
|
||||
AEE8B3442B9AA39600260C5E /* NSPasteboard+Extension.swift */,
|
||||
@ -307,6 +317,16 @@
|
||||
path = macOS;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
A54B0CE72D0CEC9800CBEFF8 /* Colorized Ghostty Icon */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
A54B0CEC2D0CFB7300CBEFF8 /* ColorizedGhosttyIcon.swift */,
|
||||
A54B0CEE2D0D2E2400CBEFF8 /* ColorizedGhosttyIconImage.swift */,
|
||||
A54B0CE82D0CECD100CBEFF8 /* ColorizedGhosttyIconView.swift */,
|
||||
);
|
||||
path = "Colorized Ghostty Icon";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
A54CD6ED299BEB14008C95BB /* Sources */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@ -580,8 +600,10 @@
|
||||
files = (
|
||||
A59630A42AF059BB00D64628 /* Ghostty.SplitNode.swift in Sources */,
|
||||
A514C8D62B54A16400493A16 /* Ghostty.Config.swift in Sources */,
|
||||
A54B0CEB2D0CFB4C00CBEFF8 /* NSImage+Extension.swift in Sources */,
|
||||
A54D786C2CA7978E001B19B1 /* BaseTerminalController.swift in Sources */,
|
||||
A59FB5CF2AE0DB50009128F3 /* InspectorView.swift in Sources */,
|
||||
A54B0CE92D0CECD100CBEFF8 /* ColorizedGhosttyIconView.swift in Sources */,
|
||||
A5D0AF3D2B37804400D21823 /* CodableBridge.swift in Sources */,
|
||||
A5D0AF3B2B36A1DE00D21823 /* TerminalRestorable.swift in Sources */,
|
||||
C1F26EA72B738B9900404083 /* NSView+Extension.swift in Sources */,
|
||||
@ -620,6 +642,8 @@
|
||||
A5CEAFDC29B8009000646FDA /* SplitView.swift in Sources */,
|
||||
A5CDF1932AAF9E0800513312 /* ConfigurationErrorsController.swift in Sources */,
|
||||
A53A6C032CCC1B7F00943E98 /* Ghostty.Action.swift in Sources */,
|
||||
A54B0CED2D0CFB7700CBEFF8 /* ColorizedGhosttyIcon.swift in Sources */,
|
||||
A54B0CEF2D0D2E2800CBEFF8 /* ColorizedGhosttyIconImage.swift in Sources */,
|
||||
A59FB5D12AE0DEA7009128F3 /* MetalView.swift in Sources */,
|
||||
A55685E029A03A9F004303CE /* AppError.swift in Sources */,
|
||||
A599CDB02CF103F60049FA26 /* NSAppearance+Extension.swift in Sources */,
|
||||
|
@ -98,6 +98,13 @@ class AppDelegate: NSObject,
|
||||
/// The observer for the app appearance.
|
||||
private var appearanceObserver: NSKeyValueObservation? = nil
|
||||
|
||||
/// The custom app icon image that is currently in use.
|
||||
@Published private(set) var appIcon: NSImage? = nil {
|
||||
didSet {
|
||||
NSApplication.shared.applicationIconImage = appIcon
|
||||
}
|
||||
}
|
||||
|
||||
override init() {
|
||||
terminalManager = TerminalManager(ghostty)
|
||||
updaterController = SPUStandardUpdaterController(
|
||||
@ -519,6 +526,22 @@ class AppDelegate: NSObject,
|
||||
} else {
|
||||
GlobalEventTap.shared.disable()
|
||||
}
|
||||
|
||||
switch (config.macosIcon) {
|
||||
case .official:
|
||||
self.appIcon = nil
|
||||
break
|
||||
|
||||
case .customStyle:
|
||||
guard let ghostColor = config.macosIconGhostColor else { break }
|
||||
guard let screenColors = config.macosIconScreenColor else { break }
|
||||
guard let icon = ColorizedGhosttyIcon(
|
||||
screenColors: screenColors,
|
||||
ghostColor: ghostColor,
|
||||
frame: config.macosIconFrame
|
||||
).makeImage() else { break }
|
||||
self.appIcon = icon
|
||||
}
|
||||
}
|
||||
|
||||
/// Sync the appearance of our app with the theme specified in the config.
|
||||
|
@ -44,7 +44,7 @@ struct AboutView: View {
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .center) {
|
||||
Image("AppIconImage")
|
||||
ghosttyIconImage()
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(height: 128)
|
||||
|
@ -0,0 +1,55 @@
|
||||
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
|
||||
|
||||
/// The frame type to use
|
||||
let frame: Ghostty.MacOSIconFrame
|
||||
|
||||
/// Make a custom colorized ghostty icon.
|
||||
func makeImage() -> NSImage? {
|
||||
// All of our layers (not in order)
|
||||
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 }
|
||||
|
||||
let baseName = switch (frame) {
|
||||
case .aluminum: "CustomIconBaseAluminum"
|
||||
case .beige: "CustomIconBaseBeige"
|
||||
case .chrome: "CustomIconBaseChrome"
|
||||
case .plastic: "CustomIconBasePlastic"
|
||||
}
|
||||
guard let base = NSImage(named: baseName) 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,
|
||||
])
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
import SwiftUI
|
||||
|
||||
extension View {
|
||||
/// Returns the ghostty icon to use for views.
|
||||
func ghosttyIconImage() -> Image {
|
||||
#if os(macOS)
|
||||
if let delegate = NSApplication.shared.delegate as? AppDelegate,
|
||||
let nsImage = delegate.appIcon {
|
||||
return Image(nsImage: nsImage)
|
||||
}
|
||||
#endif
|
||||
|
||||
return Image("AppIconImage")
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
import SwiftUI
|
||||
import Cocoa
|
||||
|
||||
// For testing.
|
||||
struct ColorizedGhosttyIconView: View {
|
||||
var body: some View {
|
||||
Image(nsImage: ColorizedGhosttyIcon(
|
||||
screenColors: [.purple, .blue],
|
||||
ghostColor: .yellow,
|
||||
frame: .aluminum
|
||||
).makeImage()!)
|
||||
}
|
||||
}
|
@ -252,6 +252,46 @@ extension Ghostty {
|
||||
return v
|
||||
}
|
||||
|
||||
var macosIcon: MacOSIcon {
|
||||
let defaultValue = MacOSIcon.official
|
||||
guard let config = self.config else { return defaultValue }
|
||||
var v: UnsafePointer<Int8>? = nil
|
||||
let key = "macos-icon"
|
||||
guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return defaultValue }
|
||||
guard let ptr = v else { return defaultValue }
|
||||
let str = String(cString: ptr)
|
||||
return MacOSIcon(rawValue: str) ?? defaultValue
|
||||
}
|
||||
|
||||
var macosIconFrame: MacOSIconFrame {
|
||||
let defaultValue = MacOSIconFrame.aluminum
|
||||
guard let config = self.config else { return defaultValue }
|
||||
var v: UnsafePointer<Int8>? = nil
|
||||
let key = "macos-icon-frame"
|
||||
guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return defaultValue }
|
||||
guard let ptr = v else { return defaultValue }
|
||||
let str = String(cString: ptr)
|
||||
return MacOSIconFrame(rawValue: str) ?? defaultValue
|
||||
}
|
||||
|
||||
var macosIconGhostColor: OSColor? {
|
||||
guard let config = self.config else { return nil }
|
||||
var v: ghostty_config_color_s = .init()
|
||||
let key = "macos-icon-ghost-color"
|
||||
guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return nil }
|
||||
return .init(ghostty: v)
|
||||
}
|
||||
|
||||
var macosIconScreenColor: [OSColor]? {
|
||||
guard let config = self.config else { return nil }
|
||||
var v: ghostty_config_color_list_s = .init()
|
||||
let key = "macos-icon-screen-color"
|
||||
guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return nil }
|
||||
guard v.len > 0 else { return nil }
|
||||
let buffer = UnsafeBufferPointer(start: v.colors, count: v.len)
|
||||
return buffer.map { .init(ghostty: $0) }
|
||||
}
|
||||
|
||||
var focusFollowsMouse : Bool {
|
||||
guard let config = self.config else { return false }
|
||||
var v = false;
|
||||
@ -261,9 +301,9 @@ extension Ghostty {
|
||||
}
|
||||
|
||||
var backgroundColor: Color {
|
||||
var rgb: UInt32 = 0
|
||||
var color: ghostty_config_color_s = .init();
|
||||
let bg_key = "background"
|
||||
if (!ghostty_config_get(config, &rgb, bg_key, UInt(bg_key.count))) {
|
||||
if (!ghostty_config_get(config, &color, bg_key, UInt(bg_key.count))) {
|
||||
#if os(macOS)
|
||||
return Color(NSColor.windowBackgroundColor)
|
||||
#elseif os(iOS)
|
||||
@ -273,14 +313,10 @@ extension Ghostty {
|
||||
#endif
|
||||
}
|
||||
|
||||
let red = Double(rgb & 0xff)
|
||||
let green = Double((rgb >> 8) & 0xff)
|
||||
let blue = Double((rgb >> 16) & 0xff)
|
||||
|
||||
return Color(
|
||||
red: red / 255,
|
||||
green: green / 255,
|
||||
blue: blue / 255
|
||||
return .init(
|
||||
red: Double(color.r) / 255,
|
||||
green: Double(color.g) / 255,
|
||||
blue: Double(color.b) / 255
|
||||
)
|
||||
}
|
||||
|
||||
@ -311,21 +347,17 @@ extension Ghostty {
|
||||
var unfocusedSplitFill: Color {
|
||||
guard let config = self.config else { return .white }
|
||||
|
||||
var rgb: UInt32 = 16777215 // white default
|
||||
var color: ghostty_config_color_s = .init();
|
||||
let key = "unfocused-split-fill"
|
||||
if (!ghostty_config_get(config, &rgb, key, UInt(key.count))) {
|
||||
if (!ghostty_config_get(config, &color, key, UInt(key.count))) {
|
||||
let bg_key = "background"
|
||||
_ = ghostty_config_get(config, &rgb, bg_key, UInt(bg_key.count));
|
||||
_ = ghostty_config_get(config, &color, bg_key, UInt(bg_key.count));
|
||||
}
|
||||
|
||||
let red = Double(rgb & 0xff)
|
||||
let green = Double((rgb >> 8) & 0xff)
|
||||
let blue = Double((rgb >> 16) & 0xff)
|
||||
|
||||
return Color(
|
||||
red: red / 255,
|
||||
green: green / 255,
|
||||
blue: blue / 255
|
||||
return .init(
|
||||
red: Double(color.r),
|
||||
green: Double(color.g) / 255,
|
||||
blue: Double(color.b) / 255
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -195,6 +195,20 @@ extension Ghostty {
|
||||
}
|
||||
}
|
||||
|
||||
/// macos-icon
|
||||
enum MacOSIcon: String {
|
||||
case official
|
||||
case customStyle = "custom-style"
|
||||
}
|
||||
|
||||
/// macos-icon-frame
|
||||
enum MacOSIconFrame: String {
|
||||
case aluminum
|
||||
case beige
|
||||
case plastic
|
||||
case chrome
|
||||
}
|
||||
|
||||
/// Enum for the macos-titlebar-proxy-icon config option
|
||||
enum MacOSTitlebarProxyIcon: String {
|
||||
case visible
|
||||
|
90
macos/Sources/Helpers/NSImage+Extension.swift
Normal 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
|
||||
}
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
import Foundation
|
||||
import GhosttyKit
|
||||
|
||||
extension OSColor {
|
||||
var isLightColor: Bool {
|
||||
@ -47,6 +48,37 @@ extension OSColor {
|
||||
#endif
|
||||
}
|
||||
|
||||
/// Create an OSColor from a hex string.
|
||||
convenience init?(hex: String) {
|
||||
var cleanedHex = hex.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
|
||||
// Remove `#` if present
|
||||
if cleanedHex.hasPrefix("#") {
|
||||
cleanedHex.removeFirst()
|
||||
}
|
||||
|
||||
guard cleanedHex.count == 6 || cleanedHex.count == 8 else { return nil }
|
||||
|
||||
let scanner = Scanner(string: cleanedHex)
|
||||
var hexNumber: UInt64 = 0
|
||||
guard scanner.scanHexInt64(&hexNumber) else { return nil }
|
||||
|
||||
let red, green, blue, alpha: CGFloat
|
||||
if cleanedHex.count == 8 {
|
||||
alpha = CGFloat((hexNumber & 0xFF000000) >> 24) / 255
|
||||
red = CGFloat((hexNumber & 0x00FF0000) >> 16) / 255
|
||||
green = CGFloat((hexNumber & 0x0000FF00) >> 8) / 255
|
||||
blue = CGFloat(hexNumber & 0x000000FF) / 255
|
||||
} else { // 6 characters
|
||||
alpha = 1.0
|
||||
red = CGFloat((hexNumber & 0xFF0000) >> 16) / 255
|
||||
green = CGFloat((hexNumber & 0x00FF00) >> 8) / 255
|
||||
blue = CGFloat(hexNumber & 0x0000FF) / 255
|
||||
}
|
||||
|
||||
self.init(red: red, green: green, blue: blue, alpha: alpha)
|
||||
}
|
||||
|
||||
func darken(by amount: CGFloat) -> OSColor {
|
||||
var h: CGFloat = 0, s: CGFloat = 0, b: CGFloat = 0, a: CGFloat = 0
|
||||
self.getHue(&h, saturation: &s, brightness: &b, alpha: &a)
|
||||
@ -58,3 +90,15 @@ extension OSColor {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Ghostty Types
|
||||
|
||||
extension OSColor {
|
||||
/// Create a color from a Ghostty color.
|
||||
convenience init(ghostty: ghostty_config_color_s) {
|
||||
let red = Double(ghostty.r) / 255
|
||||
let green = Double(ghostty.g) / 255
|
||||
let blue = Double(ghostty.b) / 255
|
||||
self.init(red: red, green: green, blue: blue, alpha: 1)
|
||||
}
|
||||
}
|
||||
|
@ -1675,6 +1675,73 @@ keybind: Keybinds = .{},
|
||||
/// you may want to disable it.
|
||||
@"macos-secure-input-indication": bool = true,
|
||||
|
||||
/// Customize the macOS app icon.
|
||||
///
|
||||
/// This only affects the icon that appears in the dock, application
|
||||
/// switcher, etc. This does not affect the icon in Finder because
|
||||
/// that is controlled by a hardcoded value in the signed application
|
||||
/// bundle and can't be changed at runtime. For more details on what
|
||||
/// exactly is affected, see the `NSApplication.icon` Apple documentation;
|
||||
/// that is the API that is being used to set the icon.
|
||||
///
|
||||
/// Valid values:
|
||||
///
|
||||
/// * `official` - Use the official Ghostty icon.
|
||||
/// * `custom-style` - Use the official Ghostty icon but with custom
|
||||
/// styles applied to various layers. The custom styles must be
|
||||
/// specified using the additional `macos-icon`-prefixed configurations.
|
||||
/// The `macos-icon-ghost-color` and `macos-icon-screen-color`
|
||||
/// configurations are required for this style.
|
||||
///
|
||||
/// WARNING: The `custom-style` option is _experimental_. We may change
|
||||
/// the format of the custom styles in the future. We're still finalizing
|
||||
/// the exact layers and customization options that will be available.
|
||||
///
|
||||
/// Other caveats:
|
||||
///
|
||||
/// * The icon in the update dialog will always be the official icon.
|
||||
/// This is because the update dialog is managed through a
|
||||
/// separate framework and cannot be customized without significant
|
||||
/// effort.
|
||||
///
|
||||
@"macos-icon": MacAppIcon = .official,
|
||||
|
||||
/// The material to use for the frame of the macOS app icon.
|
||||
///
|
||||
/// Valid values:
|
||||
///
|
||||
/// * `aluminum` - A brushed aluminum frame. This is the default.
|
||||
/// * `beige` - A classic 90's computer beige frame.
|
||||
/// * `plastic` - A glossy, dark plastic frame.
|
||||
/// * `chrome` - A shiny chrome frame.
|
||||
///
|
||||
/// This only has an effect when `macos-icon` is set to `custom-style`.
|
||||
@"macos-icon-frame": MacAppIconFrame = .aluminum,
|
||||
|
||||
/// The color of the ghost in the macOS app icon.
|
||||
///
|
||||
/// The format of the color is the same as the `background` configuration;
|
||||
/// see that for more information.
|
||||
///
|
||||
/// Note: This configuration is required when `macos-icon` is set to
|
||||
/// `custom-style`.
|
||||
///
|
||||
/// This only has an effect when `macos-icon` is set to `custom-style`.
|
||||
@"macos-icon-ghost-color": ?Color = null,
|
||||
|
||||
/// The color of the screen in the macOS app icon.
|
||||
///
|
||||
/// The screen is a gradient so you can specify multiple colors that
|
||||
/// make up the gradient. Colors should be separated by commas. The
|
||||
/// format of the color is the same as the `background` configuration;
|
||||
/// see that for more information.
|
||||
///
|
||||
/// Note: This configuration is required when `macos-icon` is set to
|
||||
/// `custom-style`.
|
||||
///
|
||||
/// This only has an effect when `macos-icon` is set to `custom-style`.
|
||||
@"macos-icon-screen-color": ?ColorList = null,
|
||||
|
||||
/// Put every surface (tab, split, window) into a dedicated Linux cgroup.
|
||||
///
|
||||
/// This makes it so that resource management can be done on a per-surface
|
||||
@ -3529,11 +3596,22 @@ pub const WindowPaddingColor = enum {
|
||||
///
|
||||
/// This is a packed struct so that the C API to read color values just
|
||||
/// works by setting it to a C integer.
|
||||
pub const Color = packed struct(u24) {
|
||||
pub const Color = struct {
|
||||
r: u8,
|
||||
g: u8,
|
||||
b: u8,
|
||||
|
||||
/// ghostty_config_color_s
|
||||
pub const C = extern struct {
|
||||
r: u8,
|
||||
g: u8,
|
||||
b: u8,
|
||||
};
|
||||
|
||||
pub fn cval(self: Color) Color.C {
|
||||
return .{ .r = self.r, .g = self.g, .b = self.b };
|
||||
}
|
||||
|
||||
/// Convert this to the terminal RGB struct
|
||||
pub fn toTerminalRGB(self: Color) terminal.color.RGB {
|
||||
return .{ .r = self.r, .g = self.g, .b = self.b };
|
||||
@ -3566,12 +3644,17 @@ pub const Color = packed struct(u24) {
|
||||
var buf: [128]u8 = undefined;
|
||||
try formatter.formatEntry(
|
||||
[]const u8,
|
||||
std.fmt.bufPrint(
|
||||
&buf,
|
||||
try self.formatBuf(&buf),
|
||||
);
|
||||
}
|
||||
|
||||
/// Format the color as a string.
|
||||
pub fn formatBuf(self: Color, buf: []u8) Allocator.Error![]const u8 {
|
||||
return std.fmt.bufPrint(
|
||||
buf,
|
||||
"#{x:0>2}{x:0>2}{x:0>2}",
|
||||
.{ self.r, self.g, self.b },
|
||||
) catch return error.OutOfMemory,
|
||||
);
|
||||
) catch error.OutOfMemory;
|
||||
}
|
||||
|
||||
/// fromHex parses a color from a hex value such as #RRGGBB. The "#"
|
||||
@ -3626,6 +3709,133 @@ pub const Color = packed struct(u24) {
|
||||
}
|
||||
};
|
||||
|
||||
pub const ColorList = struct {
|
||||
const Self = @This();
|
||||
|
||||
colors: std.ArrayListUnmanaged(Color) = .{},
|
||||
colors_c: std.ArrayListUnmanaged(Color.C) = .{},
|
||||
|
||||
/// ghostty_config_color_list_s
|
||||
pub const C = extern struct {
|
||||
colors: [*]Color.C,
|
||||
len: usize,
|
||||
};
|
||||
|
||||
pub fn cval(self: *const Self) C {
|
||||
return .{
|
||||
.colors = self.colors_c.items.ptr,
|
||||
.len = self.colors_c.items.len,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn parseCLI(
|
||||
self: *Self,
|
||||
alloc: Allocator,
|
||||
input_: ?[]const u8,
|
||||
) !void {
|
||||
const input = input_ orelse return error.ValueRequired;
|
||||
if (input.len == 0) return error.ValueRequired;
|
||||
|
||||
// Always reset on parse
|
||||
self.* = .{};
|
||||
|
||||
// Split the input by commas and parse each color
|
||||
var it = std.mem.tokenizeScalar(u8, input, ',');
|
||||
var count: usize = 0;
|
||||
while (it.next()) |raw| {
|
||||
count += 1;
|
||||
if (count > 64) return error.InvalidValue;
|
||||
|
||||
const color = try Color.parseCLI(raw);
|
||||
try self.colors.append(alloc, color);
|
||||
try self.colors_c.append(alloc, color.cval());
|
||||
}
|
||||
|
||||
// If no colors were parsed, we need to return an error
|
||||
if (self.colors.items.len == 0) return error.InvalidValue;
|
||||
|
||||
assert(self.colors.items.len == self.colors_c.items.len);
|
||||
}
|
||||
|
||||
pub fn clone(
|
||||
self: *const Self,
|
||||
alloc: Allocator,
|
||||
) Allocator.Error!Self {
|
||||
return .{
|
||||
.colors = try self.colors.clone(alloc),
|
||||
};
|
||||
}
|
||||
|
||||
/// Compare if two of our value are requal. Required by Config.
|
||||
pub fn equal(self: Self, other: Self) bool {
|
||||
const itemsA = self.colors.items;
|
||||
const itemsB = other.colors.items;
|
||||
if (itemsA.len != itemsB.len) return false;
|
||||
for (itemsA, itemsB) |a, b| {
|
||||
if (!a.equal(b)) return false;
|
||||
} else return true;
|
||||
}
|
||||
|
||||
/// Used by Formatter
|
||||
pub fn formatEntry(
|
||||
self: Self,
|
||||
formatter: anytype,
|
||||
) !void {
|
||||
// If no items, we want to render an empty field.
|
||||
if (self.colors.items.len == 0) {
|
||||
try formatter.formatEntry(void, {});
|
||||
return;
|
||||
}
|
||||
|
||||
// Build up the value of our config. Our buffer size should be
|
||||
// sized to contain all possible maximum values.
|
||||
var buf: [1024]u8 = undefined;
|
||||
var fbs = std.io.fixedBufferStream(&buf);
|
||||
var writer = fbs.writer();
|
||||
for (self.colors.items, 0..) |color, i| {
|
||||
var color_buf: [128]u8 = undefined;
|
||||
const color_str = try color.formatBuf(&color_buf);
|
||||
if (i != 0) writer.writeByte(',') catch return error.OutOfMemory;
|
||||
writer.writeAll(color_str) catch return error.OutOfMemory;
|
||||
}
|
||||
|
||||
try formatter.formatEntry(
|
||||
[]const u8,
|
||||
fbs.getWritten(),
|
||||
);
|
||||
}
|
||||
|
||||
test "parseCLI" {
|
||||
const testing = std.testing;
|
||||
var arena = ArenaAllocator.init(testing.allocator);
|
||||
defer arena.deinit();
|
||||
const alloc = arena.allocator();
|
||||
|
||||
var p: Self = .{};
|
||||
try p.parseCLI(alloc, "black,white");
|
||||
try testing.expectEqual(2, p.colors.items.len);
|
||||
|
||||
// Error cases
|
||||
try testing.expectError(error.ValueRequired, p.parseCLI(alloc, null));
|
||||
try testing.expectError(error.InvalidValue, p.parseCLI(alloc, " "));
|
||||
}
|
||||
|
||||
test "format" {
|
||||
const testing = std.testing;
|
||||
var buf = std.ArrayList(u8).init(testing.allocator);
|
||||
defer buf.deinit();
|
||||
|
||||
var arena = ArenaAllocator.init(testing.allocator);
|
||||
defer arena.deinit();
|
||||
const alloc = arena.allocator();
|
||||
|
||||
var p: Self = .{};
|
||||
try p.parseCLI(alloc, "black,white");
|
||||
try p.formatEntry(formatterpkg.entryFormatter("a", buf.writer()));
|
||||
try std.testing.expectEqualSlices(u8, "a = #000000,#ffffff\n", buf.items);
|
||||
}
|
||||
};
|
||||
|
||||
/// Palette is the 256 color palette for 256-color mode. This is still
|
||||
/// used by many terminal applications.
|
||||
pub const Palette = struct {
|
||||
@ -4906,11 +5116,29 @@ pub const MacTitlebarStyle = enum {
|
||||
};
|
||||
|
||||
/// See macos-titlebar-proxy-icon
|
||||
pub const MacTitlebarProxyIcon: type = enum {
|
||||
pub const MacTitlebarProxyIcon = enum {
|
||||
visible,
|
||||
hidden,
|
||||
};
|
||||
|
||||
/// See macos-icon
|
||||
///
|
||||
/// Note: future versions of Ghostty can support a custom icon with
|
||||
/// path by changing this to a tagged union, which doesn't change our
|
||||
/// format at all.
|
||||
pub const MacAppIcon = enum {
|
||||
official,
|
||||
@"custom-style",
|
||||
};
|
||||
|
||||
/// See macos-icon-frame
|
||||
pub const MacAppIconFrame = enum {
|
||||
aluminum,
|
||||
beige,
|
||||
plastic,
|
||||
chrome,
|
||||
};
|
||||
|
||||
/// See gtk-single-instance
|
||||
pub const GtkSingleInstance = enum {
|
||||
desktop,
|
||||
@ -5246,9 +5474,8 @@ pub const Duration = struct {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn c_get(self: Duration, ptr_raw: *anyopaque) void {
|
||||
const ptr: *usize = @ptrCast(@alignCast(ptr_raw));
|
||||
ptr.* = @intCast(self.asMilliseconds());
|
||||
pub fn cval(self: Duration) usize {
|
||||
return @intCast(self.asMilliseconds());
|
||||
}
|
||||
|
||||
/// Convenience function to convert to milliseconds since many OS and
|
||||
|
@ -60,9 +60,11 @@ fn getValue(ptr_raw: *anyopaque, value: anytype) bool {
|
||||
},
|
||||
|
||||
.Struct => |info| {
|
||||
// If the struct implements c_get then we call that
|
||||
if (@hasDecl(@TypeOf(value), "c_get")) {
|
||||
value.c_get(ptr_raw);
|
||||
// If the struct implements cval then we call then.
|
||||
if (@hasDecl(T, "cval")) {
|
||||
const PtrT = @typeInfo(@TypeOf(T.cval)).Fn.return_type.?;
|
||||
const ptr: *PtrT = @ptrCast(@alignCast(ptr_raw));
|
||||
ptr.* = value.cval();
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -100,7 +102,7 @@ fn fieldByKey(self: *const Config, comptime k: Key) Value(k) {
|
||||
return @field(self, field.name);
|
||||
}
|
||||
|
||||
test "u8" {
|
||||
test "c_get: u8" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
@ -113,7 +115,7 @@ test "u8" {
|
||||
try testing.expectEqual(@as(f32, 24), cval);
|
||||
}
|
||||
|
||||
test "enum" {
|
||||
test "c_get: enum" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
@ -128,7 +130,7 @@ test "enum" {
|
||||
try testing.expectEqualStrings("dark", str);
|
||||
}
|
||||
|
||||
test "color" {
|
||||
test "c_get: color" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
@ -136,12 +138,14 @@ test "color" {
|
||||
defer c.deinit();
|
||||
c.background = .{ .r = 255, .g = 0, .b = 0 };
|
||||
|
||||
var cval: c_uint = undefined;
|
||||
var cval: Color.C = undefined;
|
||||
try testing.expect(get(&c, .background, @ptrCast(&cval)));
|
||||
try testing.expectEqual(@as(c_uint, 255), cval);
|
||||
try testing.expectEqual(255, cval.r);
|
||||
try testing.expectEqual(0, cval.g);
|
||||
try testing.expectEqual(0, cval.b);
|
||||
}
|
||||
|
||||
test "optional" {
|
||||
test "c_get: optional" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
@ -150,14 +154,16 @@ test "optional" {
|
||||
|
||||
{
|
||||
c.@"unfocused-split-fill" = null;
|
||||
var cval: c_uint = undefined;
|
||||
var cval: Color.C = undefined;
|
||||
try testing.expect(!get(&c, .@"unfocused-split-fill", @ptrCast(&cval)));
|
||||
}
|
||||
|
||||
{
|
||||
c.@"unfocused-split-fill" = .{ .r = 255, .g = 0, .b = 0 };
|
||||
var cval: c_uint = undefined;
|
||||
var cval: Color.C = undefined;
|
||||
try testing.expect(get(&c, .@"unfocused-split-fill", @ptrCast(&cval)));
|
||||
try testing.expectEqual(@as(c_uint, 255), cval);
|
||||
try testing.expectEqual(255, cval.r);
|
||||
try testing.expectEqual(0, cval.g);
|
||||
try testing.expectEqual(0, cval.b);
|
||||
}
|
||||
}
|
||||
|