mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-04-06 07:38:21 +00:00
macOS: use NSDockTilePlugIn to update app icons (#9983)
* Using MAP prohibited `NSDockTilePlugIn` to update custom icons more reliably. And it also fixes the corner radius issue on older os: #8870 * Changes in AboutWindow cc @jparise * Start cycling with current icon * Stop cycling icons when AboutWindow is closed * Add menu to copy icon config <img width="166" height="63" alt="Xnip2025-12-20_18-40-58" src="https://github.com/user-attachments/assets/52fc1215-909e-49c7-a37a-b7c73eef61f1" /> > [!WARNING] > Upgrading from `macOS-custom-icon` needs to manually open the app once to update the icon, since this plugin is running under `com.apple.dock.external.extra.arm64`, which has sandbox enabled. > > When first upgraded to this commit, a notification about the dock plugin will pop up. The user has to enable this to change the icon smoothly. > <img width="389" height="159" alt="image" src="https://github.com/user-attachments/assets/a883ac6b-0b4d-4794-8c61-50b60707f6a2" /> Here are the testing results on [Tahoe](https://github.com/user-attachments/assets/e5fc8354-5f5c-4280-805f-88f043ceadca) and [Sequoia](https://github.com/user-attachments/assets/633d9a07-7d9d-4806-8496-82ddaffb8833): > When you see some pause in the recording, that's when I rebuild or replace the older version with the latest. This also fixes some issues when changing between different styles, consistency issues, and resetting from others to `official`. ### Developer's Note This shouldn't affect current CI flow, since this new target is just a bundle not runnable, and I tested with archiving, exporting and signing in Xcode, nothing big changed. > [!NOTE] > AI helped me to write the typo ignore-re and proofread my comments
This commit is contained in:
@@ -2,6 +2,8 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSDockTilePlugIn</key>
|
||||
<string>DockTilePlugin.plugin</string>
|
||||
<key>CFBundleDocumentTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
29C15B1D2CDC3B2900520DD4 /* bat in Resources */ = {isa = PBXBuildFile; fileRef = 29C15B1C2CDC3B2000520DD4 /* bat */; };
|
||||
55154BE02B33911F001622DC /* ghostty in Resources */ = {isa = PBXBuildFile; fileRef = 55154BDF2B33911F001622DC /* ghostty */; };
|
||||
552964E62B34A9B400030505 /* vim in Resources */ = {isa = PBXBuildFile; fileRef = 552964E52B34A9B400030505 /* vim */; };
|
||||
819324582F24E78800A9ED8F /* DockTilePlugin.plugin in Copy DockTilePlugin */ = {isa = PBXBuildFile; fileRef = 8193244D2F24E6C000A9ED8F /* DockTilePlugin.plugin */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
819324642F24FF2100A9ED8F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A5B30538299BEAAB0047F10C /* Assets.xcassets */; };
|
||||
9351BE8E3D22937F003B3499 /* nvim in Resources */ = {isa = PBXBuildFile; fileRef = 9351BE8E2D22937F003B3499 /* nvim */; };
|
||||
A51BFC272B30F1B800E92F16 /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = A51BFC262B30F1B800E92F16 /* Sparkle */; };
|
||||
A53D0C8E2B53B0EA00305CE6 /* GhosttyKit.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = A5D495A1299BEC7E00DD1313 /* GhosttyKit.xcframework */; };
|
||||
@@ -35,6 +37,13 @@
|
||||
remoteGlobalIDString = A5B30530299BEAAA0047F10C;
|
||||
remoteInfo = Ghostty;
|
||||
};
|
||||
819324672F2502FB00A9ED8F /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = A5B30529299BEAAA0047F10C /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = 8193244C2F24E6C000A9ED8F;
|
||||
remoteInfo = DockTilePlugin;
|
||||
};
|
||||
A54F45F72E1F047A0046BD5C /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = A5B30529299BEAAA0047F10C /* Project object */;
|
||||
@@ -44,12 +53,27 @@
|
||||
};
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
819324572F24E74E00A9ED8F /* Copy DockTilePlugin */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
dstPath = "";
|
||||
dstSubfolderSpec = 13;
|
||||
files = (
|
||||
819324582F24E78800A9ED8F /* DockTilePlugin.plugin in Copy DockTilePlugin */,
|
||||
);
|
||||
name = "Copy DockTilePlugin";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
29C15B1C2CDC3B2000520DD4 /* bat */ = {isa = PBXFileReference; lastKnownFileType = folder; name = bat; path = "../zig-out/share/bat"; sourceTree = "<group>"; };
|
||||
3B39CAA42B33949B00DABEB8 /* GhosttyReleaseLocal.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = GhosttyReleaseLocal.entitlements; sourceTree = "<group>"; };
|
||||
55154BDF2B33911F001622DC /* ghostty */ = {isa = PBXFileReference; lastKnownFileType = folder; name = ghostty; path = "../zig-out/share/ghostty"; sourceTree = "<group>"; };
|
||||
552964E52B34A9B400030505 /* vim */ = {isa = PBXFileReference; lastKnownFileType = folder; name = vim; path = "../zig-out/share/vim"; sourceTree = "<group>"; };
|
||||
810ACC9F2E9D3301004F8F92 /* GhosttyUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = GhosttyUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
8193244D2F24E6C000A9ED8F /* DockTilePlugin.plugin */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = DockTilePlugin.plugin; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
9351BE8E2D22937F003B3499 /* nvim */ = {isa = PBXFileReference; lastKnownFileType = folder; name = nvim; path = "../zig-out/share/nvim"; sourceTree = "<group>"; };
|
||||
A51BFC282B30F26D00E92F16 /* GhosttyDebug.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = GhosttyDebug.entitlements; sourceTree = "<group>"; };
|
||||
A546F1132D7B68D7003B11A0 /* locale */ = {isa = PBXFileReference; lastKnownFileType = folder; name = locale; path = "../zig-out/share/locale"; sourceTree = "<group>"; };
|
||||
@@ -70,6 +94,22 @@
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||
8193245D2F24E80800A9ED8F /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = {
|
||||
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
||||
membershipExceptions = (
|
||||
"Features/Custom App Icon/AppIcon.swift",
|
||||
"Features/Custom App Icon/ColorizedGhosttyIcon.swift",
|
||||
"Features/Custom App Icon/DockTilePlugin.swift",
|
||||
"Features/Custom App Icon/Extensions/Notification+AppIcon.swift",
|
||||
"Features/Custom App Icon/Extensions/UserDefaults+AppIcon.swift",
|
||||
Ghostty/Ghostty.ConfigTypes.swift,
|
||||
Ghostty/GhosttyPackageMeta.swift,
|
||||
Helpers/CrossKit.swift,
|
||||
"Helpers/Extensions/NSImage+Extension.swift",
|
||||
"Helpers/Extensions/OSColor+Extension.swift",
|
||||
);
|
||||
target = 8193244C2F24E6C000A9ED8F /* DockTilePlugin */;
|
||||
};
|
||||
81F82CB02E8281F5001EDFA7 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = {
|
||||
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
||||
membershipExceptions = (
|
||||
@@ -80,6 +120,7 @@
|
||||
Features/About/About.xib,
|
||||
Features/About/AboutController.swift,
|
||||
Features/About/AboutView.swift,
|
||||
Features/About/AboutViewModel.swift,
|
||||
Features/About/CyclingIconView.swift,
|
||||
"Features/App Intents/CloseTerminalIntent.swift",
|
||||
"Features/App Intents/CommandPaletteIntent.swift",
|
||||
@@ -96,11 +137,15 @@
|
||||
Features/ClipboardConfirmation/ClipboardConfirmation.xib,
|
||||
Features/ClipboardConfirmation/ClipboardConfirmationController.swift,
|
||||
Features/ClipboardConfirmation/ClipboardConfirmationView.swift,
|
||||
"Features/Colorized Ghostty Icon/ColorizedGhosttyIcon.swift",
|
||||
"Features/Colorized Ghostty Icon/ColorizedGhosttyIconImage.swift",
|
||||
"Features/Colorized Ghostty Icon/ColorizedGhosttyIconView.swift",
|
||||
"Features/Command Palette/CommandPalette.swift",
|
||||
"Features/Command Palette/TerminalCommandPalette.swift",
|
||||
"Features/Custom App Icon/AppIcon.swift",
|
||||
"Features/Custom App Icon/ColorizedGhosttyIcon.swift",
|
||||
"Features/Custom App Icon/ColorizedGhosttyIconImage.swift",
|
||||
"Features/Custom App Icon/ColorizedGhosttyIconView.swift",
|
||||
"Features/Custom App Icon/DockTilePlugin.swift",
|
||||
"Features/Custom App Icon/Extensions/Notification+AppIcon.swift",
|
||||
"Features/Custom App Icon/Extensions/UserDefaults+AppIcon.swift",
|
||||
"Features/Global Keybinds/GlobalEventTap.swift",
|
||||
Features/QuickTerminal/QuickTerminal.xib,
|
||||
Features/QuickTerminal/QuickTerminalController.swift,
|
||||
@@ -200,6 +245,7 @@
|
||||
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
||||
membershipExceptions = (
|
||||
App/iOS/iOSApp.swift,
|
||||
"Features/Custom App Icon/DockTilePlugin.swift",
|
||||
"Ghostty/Surface View/SurfaceView_UIKit.swift",
|
||||
);
|
||||
target = A5B30530299BEAAA0047F10C /* Ghostty */;
|
||||
@@ -208,7 +254,7 @@
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||
810ACCA02E9D3302004F8F92 /* GhosttyUITests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = GhosttyUITests; sourceTree = "<group>"; };
|
||||
81F82BC72E82815D001EDFA7 /* Sources */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (81F82CB12E8281F9001EDFA7 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, 81F82CB02E8281F5001EDFA7 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = Sources; sourceTree = "<group>"; };
|
||||
81F82BC72E82815D001EDFA7 /* Sources */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (81F82CB12E8281F9001EDFA7 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, 81F82CB02E8281F5001EDFA7 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, 8193245D2F24E80800A9ED8F /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = Sources; sourceTree = "<group>"; };
|
||||
A54F45F42E1F047A0046BD5C /* Tests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Tests; sourceTree = "<group>"; };
|
||||
/* End PBXFileSystemSynchronizedRootGroup section */
|
||||
|
||||
@@ -220,6 +266,13 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
8193244A2F24E6C000A9ED8F /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
A54F45F02E1F047A0046BD5C /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@@ -290,6 +343,7 @@
|
||||
A5D4499D2B53AE7B000F5B83 /* Ghostty-iOS.app */,
|
||||
A54F45F32E1F047A0046BD5C /* GhosttyTests.xctest */,
|
||||
810ACC9F2E9D3301004F8F92 /* GhosttyUITests.xctest */,
|
||||
8193244D2F24E6C000A9ED8F /* DockTilePlugin.plugin */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
@@ -329,6 +383,25 @@
|
||||
productReference = 810ACC9F2E9D3301004F8F92 /* GhosttyUITests.xctest */;
|
||||
productType = "com.apple.product-type.bundle.ui-testing";
|
||||
};
|
||||
8193244C2F24E6C000A9ED8F /* DockTilePlugin */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 819324512F24E6C000A9ED8F /* Build configuration list for PBXNativeTarget "DockTilePlugin" */;
|
||||
buildPhases = (
|
||||
819324492F24E6C000A9ED8F /* Sources */,
|
||||
8193244A2F24E6C000A9ED8F /* Frameworks */,
|
||||
8193244B2F24E6C000A9ED8F /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = DockTilePlugin;
|
||||
packageProductDependencies = (
|
||||
);
|
||||
productName = DockTilePlugin;
|
||||
productReference = 8193244D2F24E6C000A9ED8F /* DockTilePlugin.plugin */;
|
||||
productType = "com.apple.product-type.bundle";
|
||||
};
|
||||
A54F45F22E1F047A0046BD5C /* GhosttyTests */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = A54F45FC2E1F047A0046BD5C /* Build configuration list for PBXNativeTarget "GhosttyTests" */;
|
||||
@@ -360,10 +433,12 @@
|
||||
A5B3052D299BEAAA0047F10C /* Sources */,
|
||||
A5B3052E299BEAAA0047F10C /* Frameworks */,
|
||||
A5B3052F299BEAAA0047F10C /* Resources */,
|
||||
819324572F24E74E00A9ED8F /* Copy DockTilePlugin */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
819324682F2502FB00A9ED8F /* PBXTargetDependency */,
|
||||
);
|
||||
fileSystemSynchronizedGroups = (
|
||||
81F82BC72E82815D001EDFA7 /* Sources */,
|
||||
@@ -410,6 +485,9 @@
|
||||
CreatedOnToolsVersion = 26.1;
|
||||
TestTargetID = A5B30530299BEAAA0047F10C;
|
||||
};
|
||||
8193244C2F24E6C000A9ED8F = {
|
||||
CreatedOnToolsVersion = 26.2;
|
||||
};
|
||||
A54F45F22E1F047A0046BD5C = {
|
||||
CreatedOnToolsVersion = 26.0;
|
||||
TestTargetID = A5B30530299BEAAA0047F10C;
|
||||
@@ -441,6 +519,7 @@
|
||||
targets = (
|
||||
A5B30530299BEAAA0047F10C /* Ghostty */,
|
||||
A5D4499C2B53AE7B000F5B83 /* Ghostty-iOS */,
|
||||
8193244C2F24E6C000A9ED8F /* DockTilePlugin */,
|
||||
A54F45F22E1F047A0046BD5C /* GhosttyTests */,
|
||||
810ACC9E2E9D3301004F8F92 /* GhosttyUITests */,
|
||||
);
|
||||
@@ -455,6 +534,14 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
8193244B2F24E6C000A9ED8F /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
819324642F24FF2100A9ED8F /* Assets.xcassets in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
A54F45F12E1F047A0046BD5C /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@@ -523,6 +610,13 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
819324492F24E6C000A9ED8F /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
A54F45EF2E1F047A0046BD5C /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@@ -552,6 +646,11 @@
|
||||
target = A5B30530299BEAAA0047F10C /* Ghostty */;
|
||||
targetProxy = 810ACCA52E9D3302004F8F92 /* PBXContainerItemProxy */;
|
||||
};
|
||||
819324682F2502FB00A9ED8F /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = 8193244C2F24E6C000A9ED8F /* DockTilePlugin */;
|
||||
targetProxy = 819324672F2502FB00A9ED8F /* PBXContainerItemProxy */;
|
||||
};
|
||||
A54F45F82E1F047A0046BD5C /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = A5B30530299BEAAA0047F10C /* Ghostty */;
|
||||
@@ -684,7 +783,7 @@
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.mitchellh.GhosttyUITests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = NO;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||
@@ -707,6 +806,7 @@
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.mitchellh.GhosttyUITests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = NO;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
|
||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||
@@ -729,6 +829,7 @@
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.mitchellh.GhosttyUITests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = NO;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
|
||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||
@@ -737,6 +838,93 @@
|
||||
};
|
||||
name = ReleaseLocal;
|
||||
};
|
||||
8193244E2F24E6C000A9ED8F /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "Ghostty Dock Tile Plugin";
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
INFOPLIST_KEY_NSPrincipalClass = "";
|
||||
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Bundles";
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
||||
MARKETING_VERSION = 0.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.mitchellh.ghostty-dock-tile";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SKIP_INSTALL = YES;
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DOCK_TILE_PLUGIN DEBUG";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 6.0;
|
||||
WRAPPER_EXTENSION = plugin;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
8193244F2F24E6C000A9ED8F /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "Ghostty Dock Tile Plugin";
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
INFOPLIST_KEY_NSPrincipalClass = "";
|
||||
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Bundles";
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
||||
MARKETING_VERSION = 0.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.mitchellh.ghostty-dock-tile";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SKIP_INSTALL = YES;
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DOCK_TILE_PLUGIN;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 6.0;
|
||||
WRAPPER_EXTENSION = plugin;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
819324502F24E6C000A9ED8F /* ReleaseLocal */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "Ghostty Dock Tile Plugin";
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
INFOPLIST_KEY_NSPrincipalClass = "";
|
||||
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Bundles";
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
||||
MARKETING_VERSION = 0.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.mitchellh.ghostty-dock-tile";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SKIP_INSTALL = YES;
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DOCK_TILE_PLUGIN;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 6.0;
|
||||
WRAPPER_EXTENSION = plugin;
|
||||
};
|
||||
name = ReleaseLocal;
|
||||
};
|
||||
A54F45F92E1F047A0046BD5C /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
@@ -1163,6 +1351,16 @@
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = ReleaseLocal;
|
||||
};
|
||||
819324512F24E6C000A9ED8F /* Build configuration list for PBXNativeTarget "DockTilePlugin" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
8193244E2F24E6C000A9ED8F /* Debug */,
|
||||
8193244F2F24E6C000A9ED8F /* Release */,
|
||||
819324502F24E6C000A9ED8F /* ReleaseLocal */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = ReleaseLocal;
|
||||
};
|
||||
A54F45FC2E1F047A0046BD5C /* Build configuration list for PBXNativeTarget "GhosttyTests" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
|
||||
@@ -928,9 +928,8 @@ class AppDelegate: NSObject,
|
||||
} else {
|
||||
GlobalEventTap.shared.disable()
|
||||
}
|
||||
Task {
|
||||
await updateAppIcon(from: config)
|
||||
}
|
||||
|
||||
updateAppIcon(from: config)
|
||||
}
|
||||
|
||||
/// Sync the appearance of our app with the theme specified in the config.
|
||||
@@ -938,81 +937,15 @@ class AppDelegate: NSObject,
|
||||
NSApplication.shared.appearance = .init(ghosttyConfig: config)
|
||||
}
|
||||
|
||||
// Using AppIconActor to ensure this work
|
||||
// happens synchronously in the background
|
||||
@AppIconActor
|
||||
private func updateAppIcon(from config: Ghostty.Config) async {
|
||||
var appIcon: NSImage?
|
||||
var appIconName: String? = config.macosIcon.rawValue
|
||||
|
||||
switch config.macosIcon {
|
||||
case let icon where icon.assetName != nil:
|
||||
appIcon = NSImage(named: icon.assetName!)!
|
||||
|
||||
case .custom:
|
||||
if let userIcon = NSImage(contentsOfFile: config.macosCustomIcon) {
|
||||
appIcon = userIcon
|
||||
appIconName = config.macosCustomIcon
|
||||
} else {
|
||||
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
|
||||
appIconName = nil
|
||||
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 }
|
||||
appIcon = icon
|
||||
let colorStrings = ([ghostColor] + screenColors).compactMap(\.hexString)
|
||||
appIconName = (colorStrings + [config.macosIconFrame.rawValue])
|
||||
.joined(separator: "_")
|
||||
|
||||
default:
|
||||
// Discard saved icon name
|
||||
appIconName = nil
|
||||
private func updateAppIcon(from config: Ghostty.Config) {
|
||||
// Since this is called after `DockTilePlugin` has been running,
|
||||
// clean it up here to trigger a correct update of the current config.
|
||||
UserDefaults.standard.removeObject(forKey: "CustomGhosttyIcon")
|
||||
DispatchQueue.global().async {
|
||||
UserDefaults.standard.appIcon = AppIcon(config: config)
|
||||
DistributedNotificationCenter.default()
|
||||
.postNotificationName(.ghosttyIconDidChange, object: nil, userInfo: nil, deliverImmediately: true)
|
||||
}
|
||||
|
||||
// Only change the icon if it has actually changed from the current one,
|
||||
// or if the app build has changed (e.g. after an update that reset the icon)
|
||||
let cachedIconName = UserDefaults.standard.string(forKey: "CustomGhosttyIcon")
|
||||
let cachedIconBuild = UserDefaults.standard.string(forKey: "CustomGhosttyIconBuild")
|
||||
let currentBuild = Bundle.main.infoDictionary?["CFBundleVersion"] as? String
|
||||
let buildChanged = cachedIconBuild != currentBuild
|
||||
|
||||
guard cachedIconName != appIconName || buildChanged else {
|
||||
#if DEBUG
|
||||
if appIcon == nil {
|
||||
await MainActor.run {
|
||||
// Changing the app bundle's icon will corrupt code signing.
|
||||
// We only use the default blueprint icon for the dock,
|
||||
// so developers don't need to clean and re-build every time.
|
||||
NSApplication.shared.applicationIconImage = NSImage(named: "BlueprintImage")
|
||||
}
|
||||
}
|
||||
#endif
|
||||
return
|
||||
}
|
||||
// make it immutable, so Swift 6 won't complain
|
||||
let newIcon = appIcon
|
||||
|
||||
let appPath = Bundle.main.bundlePath
|
||||
guard NSWorkspace.shared.setIcon(newIcon, forFile: appPath, options: []) else { return }
|
||||
NSWorkspace.shared.noteFileSystemChanged(appPath)
|
||||
|
||||
await MainActor.run {
|
||||
self.appIcon = newIcon
|
||||
NSApplication.shared.applicationIconImage = newIcon
|
||||
}
|
||||
|
||||
UserDefaults.standard.set(appIconName, forKey: "CustomGhosttyIcon")
|
||||
UserDefaults.standard.set(currentBuild, forKey: "CustomGhosttyIconBuild")
|
||||
}
|
||||
|
||||
// MARK: - Restorable State
|
||||
@@ -1349,8 +1282,3 @@ private enum QuickTerminalState {
|
||||
/// Controller has been initialized.
|
||||
case initialized(QuickTerminalController)
|
||||
}
|
||||
|
||||
@globalActor
|
||||
private actor AppIconActor: GlobalActor {
|
||||
static let shared = AppIconActor()
|
||||
}
|
||||
|
||||
@@ -5,19 +5,21 @@ import SwiftUI
|
||||
class AboutController: NSWindowController, NSWindowDelegate {
|
||||
static let shared: AboutController = AboutController()
|
||||
|
||||
private let viewModel = AboutViewModel()
|
||||
override var windowNibName: NSNib.Name? { "About" }
|
||||
|
||||
override func windowDidLoad() {
|
||||
guard let window = window else { return }
|
||||
window.center()
|
||||
window.isMovableByWindowBackground = true
|
||||
window.contentView = NSHostingView(rootView: AboutView())
|
||||
window.contentView = NSHostingView(rootView: AboutView().environmentObject(viewModel))
|
||||
}
|
||||
|
||||
// MARK: - Functions
|
||||
|
||||
func show() {
|
||||
window?.makeKeyAndOrderFront(nil)
|
||||
viewModel.startCyclingIcons()
|
||||
}
|
||||
|
||||
func hide() {
|
||||
@@ -38,4 +40,8 @@ class AboutController: NSWindowController, NSWindowDelegate {
|
||||
@objc func cancel(_ sender: Any?) {
|
||||
close()
|
||||
}
|
||||
|
||||
func windowWillClose(_ notification: Notification) {
|
||||
viewModel.stopCyclingIcons()
|
||||
}
|
||||
}
|
||||
|
||||
40
macos/Sources/Features/About/AboutViewModel.swift
Normal file
40
macos/Sources/Features/About/AboutViewModel.swift
Normal file
@@ -0,0 +1,40 @@
|
||||
import Combine
|
||||
|
||||
class AboutViewModel: ObservableObject {
|
||||
@Published var currentIcon: Ghostty.MacOSIcon?
|
||||
@Published var isHovering: Bool = false
|
||||
|
||||
private var timerCancellable: AnyCancellable?
|
||||
|
||||
private let icons: [Ghostty.MacOSIcon] = [
|
||||
.official,
|
||||
.blueprint,
|
||||
.chalkboard,
|
||||
.microchip,
|
||||
.glass,
|
||||
.holographic,
|
||||
.paper,
|
||||
.retro,
|
||||
.xray,
|
||||
]
|
||||
|
||||
func startCyclingIcons() {
|
||||
timerCancellable = Timer.publish(every: 3, on: .main, in: .common)
|
||||
.autoconnect()
|
||||
.sink { [weak self] _ in
|
||||
guard let self, !isHovering else { return }
|
||||
advanceToNextIcon()
|
||||
}
|
||||
}
|
||||
|
||||
func stopCyclingIcons() {
|
||||
timerCancellable = nil
|
||||
currentIcon = nil
|
||||
}
|
||||
|
||||
func advanceToNextIcon() {
|
||||
let currentIndex = currentIcon.flatMap(icons.firstIndex(of:)) ?? 0
|
||||
let nextIndex = icons.indexWrapping(after: currentIndex)
|
||||
currentIcon = icons[nextIndex]
|
||||
}
|
||||
}
|
||||
@@ -1,50 +1,38 @@
|
||||
import SwiftUI
|
||||
import GhosttyKit
|
||||
import Combine
|
||||
|
||||
/// 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)
|
||||
@EnvironmentObject var viewModel: AboutViewModel
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
iconView(for: currentIcon)
|
||||
.id(currentIcon)
|
||||
iconView(for: viewModel.currentIcon)
|
||||
.id(viewModel.currentIcon)
|
||||
}
|
||||
.animation(.easeInOut(duration: 0.5), value: currentIcon)
|
||||
.animation(.easeInOut(duration: 0.5), value: viewModel.currentIcon)
|
||||
.frame(height: 128)
|
||||
.onReceive(timerPublisher.autoconnect()) { _ in
|
||||
if !isHovering {
|
||||
advanceToNextIcon()
|
||||
}
|
||||
}
|
||||
.onHover { hovering in
|
||||
isHovering = hovering
|
||||
viewModel.isHovering = hovering
|
||||
}
|
||||
.onTapGesture {
|
||||
advanceToNextIcon()
|
||||
viewModel.advanceToNextIcon()
|
||||
}
|
||||
.contextMenu {
|
||||
if let currentIcon = viewModel.currentIcon {
|
||||
Button("Copy Icon Config") {
|
||||
NSPasteboard.general.setString("macos-icon = \(currentIcon.rawValue)", forType: .string)
|
||||
}
|
||||
}
|
||||
}
|
||||
.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 {
|
||||
private func iconView(for icon: Ghostty.MacOSIcon?) -> some View {
|
||||
let iconImage: Image = switch icon?.assetName {
|
||||
case let assetName?: Image(assetName)
|
||||
case nil: ghosttyIconImage()
|
||||
}
|
||||
@@ -53,10 +41,4 @@ struct CyclingIconView: View {
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
}
|
||||
|
||||
private func advanceToNextIcon() {
|
||||
let currentIndex = icons.firstIndex(of: currentIcon) ?? 0
|
||||
let nextIndex = icons.indexWrapping(after: currentIndex)
|
||||
currentIcon = icons[nextIndex]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
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,
|
||||
])
|
||||
}
|
||||
}
|
||||
86
macos/Sources/Features/Custom App Icon/AppIcon.swift
Normal file
86
macos/Sources/Features/Custom App Icon/AppIcon.swift
Normal file
@@ -0,0 +1,86 @@
|
||||
import AppKit
|
||||
import System
|
||||
|
||||
/// The icon style for the Ghostty App.
|
||||
enum AppIcon: Equatable, Codable {
|
||||
case official
|
||||
case blueprint
|
||||
case chalkboard
|
||||
case glass
|
||||
case holographic
|
||||
case microchip
|
||||
case paper
|
||||
case retro
|
||||
case xray
|
||||
/// Save full image data to avoid sandboxing issues
|
||||
case custom(_ iconFile: Data)
|
||||
case customStyle(_ icon: ColorizedGhosttyIcon)
|
||||
|
||||
#if !DOCK_TILE_PLUGIN
|
||||
init?(config: Ghostty.Config) {
|
||||
switch config.macosIcon {
|
||||
case .official:
|
||||
return nil
|
||||
case .blueprint:
|
||||
self = .blueprint
|
||||
case .chalkboard:
|
||||
self = .chalkboard
|
||||
case .glass:
|
||||
self = .glass
|
||||
case .holographic:
|
||||
self = .holographic
|
||||
case .microchip:
|
||||
self = .microchip
|
||||
case .paper:
|
||||
self = .paper
|
||||
case .retro:
|
||||
self = .retro
|
||||
case .xray:
|
||||
self = .xray
|
||||
case .custom:
|
||||
if let data = try? Data(contentsOf: URL(filePath: config.macosCustomIcon, relativeTo: nil)) {
|
||||
self = .custom(data)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
case .customStyle:
|
||||
// Discard saved icon name
|
||||
// if no valid colours were found
|
||||
guard
|
||||
let ghostColor = config.macosIconGhostColor,
|
||||
let screenColors = config.macosIconScreenColor
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
self = .customStyle(ColorizedGhosttyIcon(screenColors: screenColors, ghostColor: ghostColor, frame: config.macosIconFrame))
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
func image(in bundle: Bundle) -> NSImage? {
|
||||
switch self {
|
||||
case .official:
|
||||
return nil
|
||||
case .blueprint:
|
||||
return bundle.image(forResource: "BlueprintImage")!
|
||||
case .chalkboard:
|
||||
return bundle.image(forResource: "ChalkboardImage")!
|
||||
case .glass:
|
||||
return bundle.image(forResource: "GlassImage")!
|
||||
case .holographic:
|
||||
return bundle.image(forResource: "HolographicImage")!
|
||||
case .microchip:
|
||||
return bundle.image(forResource: "MicrochipImage")!
|
||||
case .paper:
|
||||
return bundle.image(forResource: "PaperImage")!
|
||||
case .retro:
|
||||
return bundle.image(forResource: "RetroImage")!
|
||||
case .xray:
|
||||
return bundle.image(forResource: "XrayImage")!
|
||||
case let .custom(file):
|
||||
return NSImage(data: file)
|
||||
case let .customStyle(customIcon):
|
||||
return customIcon.makeImage(in: bundle)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
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(in bundle: Bundle) -> NSImage? {
|
||||
// All of our layers (not in order)
|
||||
guard let screen = bundle.image(forResource: "CustomIconScreen") else { return nil }
|
||||
guard let screenMask = bundle.image(forResource: "CustomIconScreenMask") else { return nil }
|
||||
guard let ghost = bundle.image(forResource: "CustomIconGhost") else { return nil }
|
||||
guard let crt = bundle.image(forResource: "CustomIconCRT") else { return nil }
|
||||
guard let gloss = bundle.image(forResource: "CustomIconGloss") else { return nil }
|
||||
|
||||
let baseName = switch frame {
|
||||
case .aluminum: "CustomIconBaseAluminum"
|
||||
case .beige: "CustomIconBaseBeige"
|
||||
case .chrome: "CustomIconBaseChrome"
|
||||
case .plastic: "CustomIconBasePlastic"
|
||||
}
|
||||
guard let base = bundle.image(forResource: 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,
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Codable
|
||||
|
||||
extension ColorizedGhosttyIcon: Codable {
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case version
|
||||
case screenColors
|
||||
case ghostColor
|
||||
case frame
|
||||
|
||||
static let currentVersion: Int = 1
|
||||
}
|
||||
|
||||
init(from decoder: any Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
// If no version exists then this is the legacy v0 format.
|
||||
let version = try container.decodeIfPresent(Int.self, forKey: .version) ?? 0
|
||||
guard version == 0 || version == CodingKeys.currentVersion else {
|
||||
throw DecodingError.dataCorrupted(
|
||||
DecodingError.Context(
|
||||
codingPath: decoder.codingPath,
|
||||
debugDescription: "Unsupported ColorizedGhosttyIcon version: \(version)"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
let screenColorHexes = try container.decode([String].self, forKey: .screenColors)
|
||||
let screenColors = screenColorHexes.compactMap(NSColor.init(hex:))
|
||||
let ghostColorHex = try container.decode(String.self, forKey: .ghostColor)
|
||||
guard let ghostColor = NSColor(hex: ghostColorHex) else {
|
||||
throw DecodingError.dataCorruptedError(
|
||||
forKey: .ghostColor,
|
||||
in: container,
|
||||
debugDescription: "Failed to decode ghost color from \(ghostColorHex)"
|
||||
)
|
||||
}
|
||||
let frame = try container.decode(Ghostty.MacOSIconFrame.self, forKey: .frame)
|
||||
self.init(screenColors: screenColors, ghostColor: ghostColor, frame: frame)
|
||||
}
|
||||
|
||||
func encode(to encoder: any Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(CodingKeys.currentVersion, forKey: .version)
|
||||
try container.encode(screenColors.compactMap(\.hexString), forKey: .screenColors)
|
||||
try container.encode(ghostColor.hexString, forKey: .ghostColor)
|
||||
try container.encode(frame, forKey: .frame)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: Equatable
|
||||
|
||||
extension ColorizedGhosttyIcon: Equatable {
|
||||
static func == (lhs: Self, rhs: Self) -> Bool {
|
||||
lhs.frame == rhs.frame &&
|
||||
lhs.screenColors.compactMap(\.hexString) == rhs.screenColors.compactMap(\.hexString) &&
|
||||
lhs.ghostColor.hexString == rhs.ghostColor.hexString
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,6 @@ struct ColorizedGhosttyIconView: View {
|
||||
screenColors: [.purple, .blue],
|
||||
ghostColor: .yellow,
|
||||
frame: .aluminum
|
||||
).makeImage()!)
|
||||
).makeImage(in: .main)!)
|
||||
}
|
||||
}
|
||||
118
macos/Sources/Features/Custom App Icon/DockTilePlugin.swift
Normal file
118
macos/Sources/Features/Custom App Icon/DockTilePlugin.swift
Normal file
@@ -0,0 +1,118 @@
|
||||
import AppKit
|
||||
|
||||
class DockTilePlugin: NSObject, NSDockTilePlugIn {
|
||||
// WARNING: An instance of this class is alive as long as Ghostty's icon is
|
||||
// in the doc (running or not!), so keep any state and processing to a
|
||||
// minimum to respect resource usage.
|
||||
|
||||
private let pluginBundle = Bundle(for: DockTilePlugin.self)
|
||||
|
||||
// Separate defaults based on debug vs release builds so we can test icons
|
||||
// without messing up releases.
|
||||
#if DEBUG
|
||||
private let ghosttyUserDefaults = UserDefaults(suiteName: "com.mitchellh.ghostty.debug")
|
||||
#else
|
||||
private let ghosttyUserDefaults = UserDefaults(suiteName: "com.mitchellh.ghostty")
|
||||
#endif
|
||||
|
||||
private var iconChangeObserver: Any?
|
||||
|
||||
/// The path to the Ghostty.app, determined based on the bundle path of this plugin.
|
||||
var ghosttyAppPath: String {
|
||||
var url = pluginBundle.bundleURL
|
||||
// Remove "/Contents/PlugIns/DockTilePlugIn.bundle" from the bundle URL to reach Ghostty.app.
|
||||
while url.lastPathComponent != "Ghostty.app", !url.lastPathComponent.isEmpty {
|
||||
url.deleteLastPathComponent()
|
||||
}
|
||||
return url.path
|
||||
}
|
||||
|
||||
/// The primary NSDockTilePlugin function.
|
||||
func setDockTile(_ dockTile: NSDockTile?) {
|
||||
// If no dock tile or no access to Ghostty defaults, we can't do anything.
|
||||
guard let dockTile, let ghosttyUserDefaults else {
|
||||
iconChangeObserver = nil
|
||||
return
|
||||
}
|
||||
|
||||
// Try to restore the previous icon on launch.
|
||||
iconDidChange(ghosttyUserDefaults.appIcon, dockTile: dockTile)
|
||||
|
||||
// Setup a new observer for when the icon changes so we can update. This message
|
||||
// is sent by the primary Ghostty app.
|
||||
iconChangeObserver = DistributedNotificationCenter
|
||||
.default()
|
||||
.publisher(for: .ghosttyIconDidChange)
|
||||
.map { [weak self] _ in self?.ghosttyUserDefaults?.appIcon }
|
||||
.receive(on: DispatchQueue.global())
|
||||
.sink { [weak self] newIcon in self?.iconDidChange(newIcon, dockTile: dockTile) }
|
||||
}
|
||||
|
||||
private func iconDidChange(_ newIcon: AppIcon?, dockTile: NSDockTile) {
|
||||
guard let appIcon = newIcon?.image(in: pluginBundle) else {
|
||||
resetIcon(dockTile: dockTile)
|
||||
return
|
||||
}
|
||||
|
||||
let appBundlePath = self.ghosttyAppPath
|
||||
NSWorkspace.shared.setIcon(appIcon, forFile: appBundlePath)
|
||||
NSWorkspace.shared.noteFileSystemChanged(appBundlePath)
|
||||
|
||||
dockTile.setIcon(appIcon)
|
||||
}
|
||||
|
||||
/// Reset the application icon and dock tile icon to the default.
|
||||
private func resetIcon(dockTile: NSDockTile) {
|
||||
let appBundlePath = self.ghosttyAppPath
|
||||
let appIcon: NSImage
|
||||
if #available(macOS 26.0, *) {
|
||||
// Reset to the default (glassy) icon.
|
||||
NSWorkspace.shared.setIcon(nil, forFile: appBundlePath)
|
||||
|
||||
#if DEBUG
|
||||
// Use the `Blueprint` icon to distinguish Debug from Release builds.
|
||||
appIcon = pluginBundle.image(forResource: "BlueprintImage")!
|
||||
#else
|
||||
// Get the composed icon from the app bundle.
|
||||
if let iconRep = NSWorkspace.shared.icon(forFile: appBundlePath)
|
||||
.bestRepresentation(
|
||||
for: CGRect(origin: .zero, size: dockTile.size),
|
||||
context: nil,
|
||||
hints: nil
|
||||
) {
|
||||
appIcon = NSImage(size: dockTile.size)
|
||||
appIcon.addRepresentation(iconRep)
|
||||
} else {
|
||||
// If something unexpected happens on macOS 26,
|
||||
// fall back to a bundled icon.
|
||||
appIcon = pluginBundle.image(forResource: "AppIconImage")!
|
||||
}
|
||||
#endif
|
||||
} else {
|
||||
// Use the bundled icon to keep the corner radius consistent with pre-Tahoe apps.
|
||||
appIcon = pluginBundle.image(forResource: "AppIconImage")!
|
||||
NSWorkspace.shared.setIcon(appIcon, forFile: appBundlePath)
|
||||
}
|
||||
|
||||
// Notify Finder/Dock so icon caches refresh immediately.
|
||||
NSWorkspace.shared.noteFileSystemChanged(appBundlePath)
|
||||
dockTile.setIcon(appIcon)
|
||||
}
|
||||
}
|
||||
|
||||
private extension NSDockTile {
|
||||
func setIcon(_ newIcon: NSImage) {
|
||||
// Update the Dock tile on the main thread.
|
||||
DispatchQueue.main.async {
|
||||
let iconView = NSImageView(frame: CGRect(origin: .zero, size: self.size))
|
||||
iconView.wantsLayer = true
|
||||
iconView.image = newIcon
|
||||
self.contentView = iconView
|
||||
self.display()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This is required because of the DispatchQueue call above. This doesn't
|
||||
// feel right but I don't know a better way to solve this.
|
||||
extension NSDockTile: @unchecked @retroactive Sendable {}
|
||||
@@ -0,0 +1,5 @@
|
||||
import AppKit
|
||||
|
||||
extension Notification.Name {
|
||||
static let ghosttyIconDidChange = Notification.Name("com.mitchellh.ghostty.iconDidChange")
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import AppKit
|
||||
|
||||
extension UserDefaults {
|
||||
private static let customIconKeyOld = "CustomGhosttyIcon"
|
||||
private static let customIconKeyNew = "CustomGhosttyIcon2"
|
||||
|
||||
var appIcon: AppIcon? {
|
||||
get {
|
||||
// Always remove our old pre-docktileplugin values.
|
||||
defer {
|
||||
removeObject(forKey: Self.customIconKeyOld)
|
||||
}
|
||||
|
||||
// Check if we have the new key for our dock tile plugin format.
|
||||
guard let data = data(forKey: Self.customIconKeyNew) else {
|
||||
return nil
|
||||
}
|
||||
return try? JSONDecoder().decode(AppIcon.self, from: data)
|
||||
}
|
||||
|
||||
set {
|
||||
guard let newData = try? JSONEncoder().encode(newValue) else {
|
||||
return
|
||||
}
|
||||
|
||||
set(newData, forKey: Self.customIconKeyNew)
|
||||
}
|
||||
}
|
||||
}
|
||||
43
macos/Sources/Ghostty/Ghostty.ConfigTypes.swift
Normal file
43
macos/Sources/Ghostty/Ghostty.ConfigTypes.swift
Normal file
@@ -0,0 +1,43 @@
|
||||
// This file contains the configuration types for Ghostty so that alternate targets
|
||||
// can get typed information without depending on all the dependencies of GhosttyKit.
|
||||
|
||||
extension Ghostty {
|
||||
/// macos-icon
|
||||
enum MacOSIcon: String, Sendable {
|
||||
case official
|
||||
case blueprint
|
||||
case chalkboard
|
||||
case glass
|
||||
case holographic
|
||||
case microchip
|
||||
case paper
|
||||
case retro
|
||||
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
|
||||
enum MacOSIconFrame: String, Codable {
|
||||
case aluminum
|
||||
case beige
|
||||
case plastic
|
||||
case chrome
|
||||
}
|
||||
}
|
||||
@@ -2,23 +2,6 @@ import os
|
||||
import SwiftUI
|
||||
import GhosttyKit
|
||||
|
||||
struct Ghostty {
|
||||
// The primary logger used by the GhosttyKit libraries.
|
||||
static let logger = Logger(
|
||||
subsystem: Bundle.main.bundleIdentifier!,
|
||||
category: "ghostty"
|
||||
)
|
||||
|
||||
// All the notifications that will be emitted will be put here.
|
||||
struct Notification {}
|
||||
|
||||
// The user notification category identifier
|
||||
static let userNotificationCategory = "com.mitchellh.ghostty.userNotification"
|
||||
|
||||
// The user notification "Show" action
|
||||
static let userNotificationActionShow = "com.mitchellh.ghostty.userNotification.Show"
|
||||
}
|
||||
|
||||
// MARK: C Extensions
|
||||
|
||||
/// A command is fully self-contained so it is Sendable.
|
||||
@@ -28,6 +11,14 @@ extension ghostty_command_s: @unchecked @retroactive Sendable {}
|
||||
/// may be unsafe but the value itself is safe to send across threads.
|
||||
extension ghostty_surface_t: @unchecked @retroactive Sendable {}
|
||||
|
||||
extension Ghostty {
|
||||
// The user notification category identifier
|
||||
static let userNotificationCategory = "com.mitchellh.ghostty.userNotification"
|
||||
|
||||
// The user notification "Show" action
|
||||
static let userNotificationActionShow = "com.mitchellh.ghostty.userNotification.Show"
|
||||
}
|
||||
|
||||
// MARK: Build Info
|
||||
|
||||
extension Ghostty {
|
||||
@@ -317,45 +308,6 @@ extension Ghostty {
|
||||
}
|
||||
}
|
||||
|
||||
/// macos-icon
|
||||
enum MacOSIcon: String, Sendable {
|
||||
case official
|
||||
case blueprint
|
||||
case chalkboard
|
||||
case glass
|
||||
case holographic
|
||||
case microchip
|
||||
case paper
|
||||
case retro
|
||||
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
|
||||
enum MacOSIconFrame: String {
|
||||
case aluminum
|
||||
case beige
|
||||
case plastic
|
||||
case chrome
|
||||
}
|
||||
|
||||
/// Enum for the macos-window-buttons config option
|
||||
enum MacOSWindowButtons: String {
|
||||
case visible
|
||||
16
macos/Sources/Ghostty/GhosttyPackageMeta.swift
Normal file
16
macos/Sources/Ghostty/GhosttyPackageMeta.swift
Normal file
@@ -0,0 +1,16 @@
|
||||
import Foundation
|
||||
import os
|
||||
|
||||
// This defines the minimal information required so all other files can do
|
||||
// `extension Ghostty` to add more to it. This purposely has minimal
|
||||
// dependencies so things like our dock tile plugin can use it.
|
||||
enum Ghostty {
|
||||
// The primary logger used by the GhosttyKit libraries.
|
||||
static let logger = Logger(
|
||||
subsystem: Bundle.main.bundleIdentifier!,
|
||||
category: "ghostty"
|
||||
)
|
||||
|
||||
// All the notifications that will be emitted will be put here.
|
||||
struct Notification {}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import Foundation
|
||||
#if !DOCK_TILE_PLUGIN
|
||||
import GhosttyKit
|
||||
#endif
|
||||
|
||||
extension OSColor {
|
||||
var isLightColor: Bool {
|
||||
@@ -92,7 +94,7 @@ extension OSColor {
|
||||
}
|
||||
|
||||
// MARK: Ghostty Types
|
||||
|
||||
#if !DOCK_TILE_PLUGIN
|
||||
extension OSColor {
|
||||
/// Create a color from a Ghostty color.
|
||||
convenience init(ghostty: ghostty_config_color_s) {
|
||||
@@ -102,3 +104,4 @@ extension OSColor {
|
||||
self.init(red: red, green: green, blue: blue, alpha: 1)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
144
macos/Tests/ColorizedGhosttyIconTests.swift
Normal file
144
macos/Tests/ColorizedGhosttyIconTests.swift
Normal file
@@ -0,0 +1,144 @@
|
||||
import AppKit
|
||||
import Foundation
|
||||
import Testing
|
||||
@testable import Ghostty
|
||||
|
||||
struct ColorizedGhosttyIconTests {
|
||||
private func makeIcon(
|
||||
screenColors: [NSColor] = [
|
||||
NSColor(hex: "#112233")!,
|
||||
NSColor(hex: "#AABBCC")!,
|
||||
],
|
||||
ghostColor: NSColor = NSColor(hex: "#445566")!,
|
||||
frame: Ghostty.MacOSIconFrame = .aluminum
|
||||
) -> ColorizedGhosttyIcon {
|
||||
.init(screenColors: screenColors, ghostColor: ghostColor, frame: frame)
|
||||
}
|
||||
|
||||
// MARK: - Codable
|
||||
|
||||
@Test func codableRoundTripPreservesIcon() throws {
|
||||
let icon = makeIcon(frame: .chrome)
|
||||
let data = try JSONEncoder().encode(icon)
|
||||
let decoded = try JSONDecoder().decode(ColorizedGhosttyIcon.self, from: data)
|
||||
|
||||
#expect(decoded == icon)
|
||||
#expect(decoded.screenColors.compactMap(\.hexString) == ["#112233", "#AABBCC"])
|
||||
#expect(decoded.ghostColor.hexString == "#445566")
|
||||
#expect(decoded.frame == .chrome)
|
||||
}
|
||||
|
||||
@Test func encodingWritesVersionAndHexColors() throws {
|
||||
let icon = makeIcon(frame: .plastic)
|
||||
let data = try JSONEncoder().encode(icon)
|
||||
|
||||
let payload = try #require(JSONSerialization.jsonObject(with: data) as? [String: Any])
|
||||
#expect(payload["version"] as? Int == 1)
|
||||
#expect(payload["screenColors"] as? [String] == ["#112233", "#AABBCC"])
|
||||
#expect(payload["ghostColor"] as? String == "#445566")
|
||||
#expect(payload["frame"] as? String == "plastic")
|
||||
}
|
||||
|
||||
@Test func decodesLegacyV0PayloadWithoutVersion() throws {
|
||||
let data = Data("""
|
||||
{
|
||||
"screenColors": ["#112233", "#AABBCC"],
|
||||
"ghostColor": "#445566",
|
||||
"frame": "beige"
|
||||
}
|
||||
""".utf8)
|
||||
|
||||
let decoded = try JSONDecoder().decode(ColorizedGhosttyIcon.self, from: data)
|
||||
#expect(decoded.screenColors.compactMap(\.hexString) == ["#112233", "#AABBCC"])
|
||||
#expect(decoded.ghostColor.hexString == "#445566")
|
||||
#expect(decoded.frame == .beige)
|
||||
}
|
||||
|
||||
@Test func decodingUnsupportedVersionThrowsDataCorrupted() {
|
||||
let data = Data("""
|
||||
{
|
||||
"version": 99,
|
||||
"screenColors": ["#112233", "#AABBCC"],
|
||||
"ghostColor": "#445566",
|
||||
"frame": "chrome"
|
||||
}
|
||||
""".utf8)
|
||||
|
||||
do {
|
||||
_ = try JSONDecoder().decode(ColorizedGhosttyIcon.self, from: data)
|
||||
Issue.record("Expected decode to fail for unsupported version")
|
||||
} catch let DecodingError.dataCorrupted(context) {
|
||||
#expect(context.debugDescription.contains("Unsupported ColorizedGhosttyIcon version"))
|
||||
} catch {
|
||||
Issue.record("Expected DecodingError.dataCorrupted, got: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
@Test func decodingInvalidGhostColorThrows() {
|
||||
let data = Data("""
|
||||
{
|
||||
"version": 1,
|
||||
"screenColors": ["#112233", "#AABBCC"],
|
||||
"ghostColor": "not-a-color",
|
||||
"frame": "chrome"
|
||||
}
|
||||
""".utf8)
|
||||
|
||||
do {
|
||||
_ = try JSONDecoder().decode(ColorizedGhosttyIcon.self, from: data)
|
||||
Issue.record("Expected decode to fail for invalid ghost color")
|
||||
} catch let DecodingError.dataCorrupted(context) {
|
||||
#expect(context.debugDescription.contains("Failed to decode ghost color"))
|
||||
} catch {
|
||||
Issue.record("Expected DecodingError.dataCorrupted, got: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
@Test func decodingInvalidScreenColorsDropsInvalidEntries() throws {
|
||||
let data = Data("""
|
||||
{
|
||||
"version": 1,
|
||||
"screenColors": ["#112233", "invalid", "#AABBCC"],
|
||||
"ghostColor": "#445566",
|
||||
"frame": "chrome"
|
||||
}
|
||||
""".utf8)
|
||||
|
||||
let decoded = try JSONDecoder().decode(ColorizedGhosttyIcon.self, from: data)
|
||||
#expect(decoded.screenColors.compactMap(\.hexString) == ["#112233", "#AABBCC"])
|
||||
}
|
||||
|
||||
// MARK: - Equatable
|
||||
|
||||
@Test func equatableUsesHexColorAndFrameValues() {
|
||||
let lhs = makeIcon(
|
||||
screenColors: [
|
||||
NSColor(red: 0x11 / 255.0, green: 0x22 / 255.0, blue: 0x33 / 255.0, alpha: 1.0),
|
||||
NSColor(red: 0xAA / 255.0, green: 0xBB / 255.0, blue: 0xCC / 255.0, alpha: 1.0),
|
||||
],
|
||||
ghostColor: NSColor(red: 0x44 / 255.0, green: 0x55 / 255.0, blue: 0x66 / 255.0, alpha: 1.0),
|
||||
frame: .chrome
|
||||
)
|
||||
let rhs = makeIcon(frame: .chrome)
|
||||
|
||||
#expect(lhs == rhs)
|
||||
}
|
||||
|
||||
@Test func equatableReturnsFalseForDifferentFrame() {
|
||||
let lhs = makeIcon(frame: .aluminum)
|
||||
let rhs = makeIcon(frame: .chrome)
|
||||
#expect(lhs != rhs)
|
||||
}
|
||||
|
||||
@Test func equatableReturnsFalseForDifferentScreenColors() {
|
||||
let lhs = makeIcon(screenColors: [NSColor(hex: "#112233")!, NSColor(hex: "#AABBCC")!])
|
||||
let rhs = makeIcon(screenColors: [NSColor(hex: "#112233")!, NSColor(hex: "#CCBBAA")!])
|
||||
#expect(lhs != rhs)
|
||||
}
|
||||
|
||||
@Test func equatableReturnsFalseForDifferentGhostColor() {
|
||||
let lhs = makeIcon(ghostColor: NSColor(hex: "#445566")!)
|
||||
let rhs = makeIcon(ghostColor: NSColor(hex: "#665544")!)
|
||||
#expect(lhs != rhs)
|
||||
}
|
||||
}
|
||||
@@ -42,6 +42,8 @@ extend-ignore-re = [
|
||||
"draw[0-9A-F]+(_[0-9A-F]+)?\\(",
|
||||
# Ignore test data in src/input/paste.zig
|
||||
"\"hel\\\\x",
|
||||
# Ignore long hex-like IDs such as 815E26BA2EF1E00F005C67B1
|
||||
"[0-9A-F]{12,}",
|
||||
]
|
||||
|
||||
[default.extend-words]
|
||||
|
||||
Reference in New Issue
Block a user