Fix Dock persistence for manual app icons (#2360)
This commit is contained in:
parent
2c5c4fcf8d
commit
0666a98ae9
5 changed files with 367 additions and 13 deletions
|
|
@ -26,6 +26,8 @@
|
|||
A5001226 /* SocketControlSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001225 /* SocketControlSettings.swift */; };
|
||||
A5001601 /* SentryHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001600 /* SentryHelper.swift */; };
|
||||
A5001621 /* AppleScriptSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001620 /* AppleScriptSupport.swift */; };
|
||||
D1320AA0D1320AA0D1320AA1 /* AppIconDockTilePlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1320AA0D1320AA0D1320AA4 /* AppIconDockTilePlugin.swift */; };
|
||||
D1320AA0D1320AA0D1320AA2 /* CmuxDockTilePlugin.plugin in Copy Dock Tile Plugin */ = {isa = PBXBuildFile; fileRef = D1320AA0D1320AA0D1320AA5 /* CmuxDockTilePlugin.plugin */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
A5001400 /* Panel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001410 /* Panel.swift */; };
|
||||
A5001401 /* TerminalPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001411 /* TerminalPanel.swift */; };
|
||||
A5001402 /* BrowserPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001412 /* BrowserPanel.swift */; };
|
||||
|
|
@ -154,6 +156,17 @@
|
|||
name = "Copy CLI";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
D1320AA0D1320AA0D1320AA6 /* Copy Dock Tile Plugin */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
dstPath = "";
|
||||
dstSubfolderSpec = 13;
|
||||
files = (
|
||||
D1320AA0D1320AA0D1320AA2 /* CmuxDockTilePlugin.plugin in Copy Dock Tile Plugin */,
|
||||
);
|
||||
name = "Copy Dock Tile Plugin";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
|
|
@ -178,6 +191,13 @@
|
|||
remoteGlobalIDString = A5001050 /* GhosttyTabs */;
|
||||
remoteInfo = GhosttyTabs;
|
||||
};
|
||||
D1320AA0D1320AA0D1320AA3 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = A5001070 /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = D1320AA0D1320AA0D1320AA8 /* CmuxDockTilePlugin */;
|
||||
remoteInfo = CmuxDockTilePlugin;
|
||||
};
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
|
|
@ -202,6 +222,7 @@
|
|||
A5001019 /* TerminalController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalController.swift; sourceTree = "<group>"; };
|
||||
A5001600 /* SentryHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryHelper.swift; sourceTree = "<group>"; };
|
||||
A5001620 /* AppleScriptSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppleScriptSupport.swift; sourceTree = "<group>"; };
|
||||
D1320AA0D1320AA0D1320AA4 /* AppIconDockTilePlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIconDockTilePlugin.swift; sourceTree = "<group>"; };
|
||||
A5001510 /* CmuxWebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Panels/CmuxWebView.swift; sourceTree = "<group>"; };
|
||||
A5001511 /* UITestRecorder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITestRecorder.swift; sourceTree = "<group>"; };
|
||||
A5001520 /* PostHogAnalytics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogAnalytics.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -258,6 +279,7 @@
|
|||
A5002001 /* THIRD_PARTY_LICENSES.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = THIRD_PARTY_LICENSES.md; sourceTree = SOURCE_ROOT; };
|
||||
B9000001A1B2C3D4E5F60719 /* cmux.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = cmux.swift; sourceTree = "<group>"; };
|
||||
B9000004A1B2C3D4E5F60719 /* cmux */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = cmux; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
D1320AA0D1320AA0D1320AA5 /* CmuxDockTilePlugin.plugin */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CmuxDockTilePlugin.plugin; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
B9000011A1B2C3D4E5F60719 /* AutomationSocketUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutomationSocketUITests.swift; sourceTree = "<group>"; };
|
||||
B9000013A1B2C3D4E5F60719 /* JumpToUnreadUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JumpToUnreadUITests.swift; sourceTree = "<group>"; };
|
||||
B9000016A1B2C3D4E5F60719 /* MultiWindowNotificationsUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiWindowNotificationsUITests.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -339,6 +361,13 @@
|
|||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
D1320AA0D1320AA0D1320AA7 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
|
|
@ -369,6 +398,13 @@
|
|||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
D1320AA0D1320AA0D1320AA9 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
|
|
@ -435,6 +471,7 @@
|
|||
A5001225 /* SocketControlSettings.swift */,
|
||||
A5001600 /* SentryHelper.swift */,
|
||||
A5001620 /* AppleScriptSupport.swift */,
|
||||
D1320AA0D1320AA0D1320AA4 /* AppIconDockTilePlugin.swift */,
|
||||
A5001090 /* AppDelegate.swift */,
|
||||
A5001091 /* NotificationsPage.swift */,
|
||||
A5001092 /* TerminalNotificationStore.swift */,
|
||||
|
|
@ -501,6 +538,7 @@
|
|||
children = (
|
||||
A5001000 /* cmux.app */,
|
||||
B9000004A1B2C3D4E5F60719 /* cmux */,
|
||||
D1320AA0D1320AA0D1320AA5 /* CmuxDockTilePlugin.plugin */,
|
||||
7E7E6EF344A568AC7FEE3715 /* cmuxUITests.xctest */,
|
||||
F1000002A1B2C3D4E5F60718 /* cmuxTests.xctest */,
|
||||
);
|
||||
|
|
@ -576,6 +614,7 @@
|
|||
A5001051 /* Sources */,
|
||||
A5001030 /* Frameworks */,
|
||||
A5001102 /* Resources */,
|
||||
D1320AA0D1320AA0D1320AA6 /* Copy Dock Tile Plugin */,
|
||||
A5001020 /* Embed Frameworks */,
|
||||
B900000AA1B2C3D4E5F60719 /* Copy CLI */,
|
||||
A5001300A1B2C3D4E5F60719 /* Copy Ghostty Resources */,
|
||||
|
|
@ -584,6 +623,7 @@
|
|||
);
|
||||
dependencies = (
|
||||
B900000EA1B2C3D4E5F60719 /* PBXTargetDependency */,
|
||||
D1320AA0D1320AA0D1320AB1 /* PBXTargetDependency */,
|
||||
);
|
||||
packageProductDependencies = (
|
||||
A5001231 /* Sparkle */,
|
||||
|
|
@ -597,6 +637,23 @@
|
|||
productReference = A5001000 /* cmux.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
D1320AA0D1320AA0D1320AA8 /* CmuxDockTilePlugin */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = D1320AA0D1320AA0D1320AB4 /* Build configuration list for PBXNativeTarget "CmuxDockTilePlugin" */;
|
||||
buildPhases = (
|
||||
D1320AA0D1320AA0D1320AB0 /* Sources */,
|
||||
D1320AA0D1320AA0D1320AA7 /* Frameworks */,
|
||||
D1320AA0D1320AA0D1320AA9 /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = CmuxDockTilePlugin;
|
||||
productName = CmuxDockTilePlugin;
|
||||
productReference = D1320AA0D1320AA0D1320AA5 /* CmuxDockTilePlugin.plugin */;
|
||||
productType = "com.apple.product-type.bundle";
|
||||
};
|
||||
B9000005A1B2C3D4E5F60719 /* cmux-cli */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = B9000007A1B2C3D4E5F60719 /* Build configuration list for PBXNativeTarget "cmux-cli" */;
|
||||
|
|
@ -700,6 +757,7 @@
|
|||
projectRoot = "";
|
||||
targets = (
|
||||
A5001050 /* GhosttyTabs */,
|
||||
D1320AA0D1320AA0D1320AA8 /* CmuxDockTilePlugin */,
|
||||
B9000005A1B2C3D4E5F60719 /* cmux-cli */,
|
||||
CB450DF0F0B3839599082C4D /* cmuxUITests */,
|
||||
F1000004A1B2C3D4E5F60718 /* cmuxTests */,
|
||||
|
|
@ -773,6 +831,14 @@
|
|||
A5001654 /* CmuxDirectoryTrust.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
D1320AA0D1320AA0D1320AB0 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
D1320AA0D1320AA0D1320AA1 /* AppIconDockTilePlugin.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
E436EF0BA8EC9E6721A42F79 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
|
|
@ -860,6 +926,11 @@
|
|||
target = B9000005A1B2C3D4E5F60719 /* cmux-cli */;
|
||||
targetProxy = B900000DA1B2C3D4E5F60719 /* PBXContainerItemProxy */;
|
||||
};
|
||||
D1320AA0D1320AA0D1320AB1 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = D1320AA0D1320AA0D1320AA8 /* CmuxDockTilePlugin */;
|
||||
targetProxy = D1320AA0D1320AA0D1320AA3 /* PBXContainerItemProxy */;
|
||||
};
|
||||
/* End PBXTargetDependency section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
|
|
@ -1004,6 +1075,55 @@
|
|||
};
|
||||
name = Release;
|
||||
};
|
||||
D1320AA0D1320AA0D1320AB2 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 78;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "cmux Dock Tile Plugin";
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
INFOPLIST_KEY_NSPrincipalClass = "";
|
||||
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Bundles";
|
||||
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
||||
MARKETING_VERSION = 0.63.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.cmuxterm.app.docktileplugin.debug;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
WRAPPER_EXTENSION = plugin;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
D1320AA0D1320AA0D1320AB3 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 78;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "cmux Dock Tile Plugin";
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
INFOPLIST_KEY_NSPrincipalClass = "";
|
||||
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Bundles";
|
||||
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
||||
MARKETING_VERSION = 0.63.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.cmuxterm.app.docktileplugin;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
WRAPPER_EXTENSION = plugin;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
B9000008A1B2C3D4E5F60719 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
|
|
@ -1192,6 +1312,15 @@
|
|||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
D1320AA0D1320AA0D1320AB4 /* Build configuration list for PBXNativeTarget "CmuxDockTilePlugin" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
D1320AA0D1320AA0D1320AB2 /* Debug */,
|
||||
D1320AA0D1320AA0D1320AB3 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
AD2C7ED08993D3CD4910A1FF /* Build configuration list for PBXNativeTarget "cmuxUITests" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
|
|
|
|||
|
|
@ -35,6 +35,8 @@
|
|||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>NSDockTilePlugIn</key>
|
||||
<string>CmuxDockTilePlugin.plugin</string>
|
||||
<key>LSApplicationCategoryType</key>
|
||||
<string>public.app-category.developer-tools</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
|
|
|
|||
120
Sources/AppIconDockTilePlugin.swift
Normal file
120
Sources/AppIconDockTilePlugin.swift
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
import AppKit
|
||||
|
||||
private let cmuxAppIconDidChangeNotification = Notification.Name("com.cmuxterm.appIconDidChange")
|
||||
private let cmuxAppIconModeKey = "appIconMode"
|
||||
|
||||
private enum DockTileAppIconMode: String {
|
||||
case automatic
|
||||
case light
|
||||
case dark
|
||||
|
||||
init(defaultsValue: String?) {
|
||||
self = Self(rawValue: defaultsValue ?? "") ?? .automatic
|
||||
}
|
||||
|
||||
var imageName: NSImage.Name? {
|
||||
switch self {
|
||||
case .automatic:
|
||||
return nil
|
||||
case .light:
|
||||
return NSImage.Name("AppIconLight")
|
||||
case .dark:
|
||||
return NSImage.Name("AppIconDark")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class CmuxDockTilePlugin: NSObject, NSDockTilePlugIn {
|
||||
// The plugin can stay alive while the app remains in the Dock, even after quit.
|
||||
// Keep the state minimal and derive everything from the enclosing app bundle.
|
||||
private let pluginBundle = Bundle(for: CmuxDockTilePlugin.self)
|
||||
private var iconChangeObserver: NSObjectProtocol?
|
||||
|
||||
deinit {
|
||||
if let iconChangeObserver {
|
||||
DistributedNotificationCenter.default().removeObserver(iconChangeObserver)
|
||||
}
|
||||
}
|
||||
|
||||
func setDockTile(_ dockTile: NSDockTile?) {
|
||||
if let iconChangeObserver {
|
||||
DistributedNotificationCenter.default().removeObserver(iconChangeObserver)
|
||||
self.iconChangeObserver = nil
|
||||
}
|
||||
|
||||
guard let dockTile else { return }
|
||||
updateDockTile(dockTile)
|
||||
|
||||
iconChangeObserver = DistributedNotificationCenter.default().addObserver(
|
||||
forName: cmuxAppIconDidChangeNotification,
|
||||
object: nil,
|
||||
queue: nil
|
||||
) { [weak self] _ in
|
||||
guard let self else { return }
|
||||
self.updateDockTile(dockTile)
|
||||
}
|
||||
}
|
||||
|
||||
private var appBundleURL: URL? {
|
||||
Self.appBundleURL(for: pluginBundle.bundleURL)
|
||||
}
|
||||
|
||||
private var appBundle: Bundle? {
|
||||
guard let appBundleURL else { return nil }
|
||||
return Bundle(url: appBundleURL)
|
||||
}
|
||||
|
||||
private var appDefaults: UserDefaults? {
|
||||
guard let bundleIdentifier = appBundle?.bundleIdentifier else { return nil }
|
||||
return UserDefaults(suiteName: bundleIdentifier)
|
||||
}
|
||||
|
||||
private func updateDockTile(_ dockTile: NSDockTile) {
|
||||
let mode = DockTileAppIconMode(defaultsValue: appDefaults?.string(forKey: cmuxAppIconModeKey))
|
||||
guard let imageName = mode.imageName,
|
||||
let icon = appBundle?.image(forResource: imageName) else {
|
||||
dockTile.showDefaultAppIcon()
|
||||
return
|
||||
}
|
||||
|
||||
dockTile.showIcon(icon)
|
||||
}
|
||||
|
||||
/// Determine the enclosing app bundle for the dock tile plugin bundle.
|
||||
static func appBundleURL(for pluginBundleURL: URL) -> URL? {
|
||||
var url = pluginBundleURL
|
||||
while true {
|
||||
if url.pathExtension.compare("app", options: .caseInsensitive) == .orderedSame {
|
||||
return url
|
||||
}
|
||||
|
||||
let parent = url.deletingLastPathComponent()
|
||||
if parent.path == url.path {
|
||||
return nil
|
||||
}
|
||||
|
||||
url = parent
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension NSDockTile {
|
||||
func showDefaultAppIcon() {
|
||||
DispatchQueue.main.async {
|
||||
self.contentView = nil
|
||||
self.display()
|
||||
}
|
||||
}
|
||||
|
||||
func showIcon(_ newIcon: NSImage) {
|
||||
DispatchQueue.main.async {
|
||||
let iconView = NSImageView(frame: CGRect(origin: .zero, size: self.size))
|
||||
iconView.wantsLayer = true
|
||||
iconView.image = newIcon
|
||||
self.contentView = iconView
|
||||
self.display()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension NSDockTile: @unchecked @retroactive Sendable {}
|
||||
|
|
@ -3699,6 +3699,41 @@ enum AppIconMode: String, CaseIterable, Identifiable {
|
|||
enum AppIconSettings {
|
||||
static let modeKey = "appIconMode"
|
||||
static let defaultMode: AppIconMode = .automatic
|
||||
private static let dockTileIconDidChangeNotification = Notification.Name("com.cmuxterm.appIconDidChange")
|
||||
|
||||
struct Environment {
|
||||
let imageForMode: (AppIconMode) -> NSImage?
|
||||
let setApplicationIconImage: (NSImage) -> Void
|
||||
let startAppearanceObservation: () -> Void
|
||||
let stopAppearanceObservation: () -> Void
|
||||
let notifyDockTilePlugin: () -> Void
|
||||
|
||||
static func live() -> Self {
|
||||
Self(
|
||||
imageForMode: { mode in
|
||||
guard let imageName = mode.imageName else { return nil }
|
||||
return NSImage(named: imageName)
|
||||
},
|
||||
setApplicationIconImage: { icon in
|
||||
NSApplication.shared.applicationIconImage = icon
|
||||
},
|
||||
startAppearanceObservation: {
|
||||
AppIconAppearanceObserver.shared.startObserving()
|
||||
},
|
||||
stopAppearanceObservation: {
|
||||
AppIconAppearanceObserver.shared.stopObserving()
|
||||
},
|
||||
notifyDockTilePlugin: {
|
||||
DistributedNotificationCenter.default().postNotificationName(
|
||||
AppIconSettings.dockTileIconDidChangeNotification,
|
||||
object: nil,
|
||||
userInfo: nil,
|
||||
deliverImmediately: true
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
static func resolvedMode(defaults: UserDefaults = .standard) -> AppIconMode {
|
||||
guard let raw = defaults.string(forKey: modeKey),
|
||||
|
|
@ -3708,21 +3743,21 @@ enum AppIconSettings {
|
|||
return mode
|
||||
}
|
||||
|
||||
static func applyIcon(_ mode: AppIconMode) {
|
||||
static func applyIcon(_ mode: AppIconMode, environment: Environment = .live()) {
|
||||
switch mode {
|
||||
case .automatic:
|
||||
AppIconAppearanceObserver.shared.startObserving()
|
||||
environment.startAppearanceObservation()
|
||||
case .light:
|
||||
AppIconAppearanceObserver.shared.stopObserving()
|
||||
if let icon = NSImage(named: "AppIconLight") {
|
||||
NSApplication.shared.applicationIconImage = icon
|
||||
}
|
||||
environment.stopAppearanceObservation()
|
||||
guard let icon = environment.imageForMode(.light) else { return }
|
||||
environment.setApplicationIconImage(icon)
|
||||
case .dark:
|
||||
AppIconAppearanceObserver.shared.stopObserving()
|
||||
if let icon = NSImage(named: "AppIconDark") {
|
||||
NSApplication.shared.applicationIconImage = icon
|
||||
}
|
||||
environment.stopAppearanceObservation()
|
||||
guard let icon = environment.imageForMode(.dark) else { return }
|
||||
environment.setApplicationIconImage(icon)
|
||||
}
|
||||
|
||||
environment.notifyDockTilePlugin()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,74 @@ import UserNotifications
|
|||
@testable import cmux
|
||||
#endif
|
||||
|
||||
@MainActor
|
||||
final class AppIconSettingsTests: XCTestCase {
|
||||
func testApplyDarkSetsRuntimeIconAndNotifiesDockTilePlugin() {
|
||||
let expectedIcon = NSImage(size: NSSize(width: 16, height: 16))
|
||||
var receivedRuntimeIcon: NSImage?
|
||||
var dockTileNotificationCount = 0
|
||||
var startObservationCallCount = 0
|
||||
var stopObservationCallCount = 0
|
||||
|
||||
let environment = AppIconSettings.Environment(
|
||||
imageForMode: { mode in
|
||||
XCTAssertEqual(mode, .dark)
|
||||
return expectedIcon
|
||||
},
|
||||
setApplicationIconImage: { icon in
|
||||
receivedRuntimeIcon = icon
|
||||
},
|
||||
startAppearanceObservation: {
|
||||
startObservationCallCount += 1
|
||||
},
|
||||
stopAppearanceObservation: {
|
||||
stopObservationCallCount += 1
|
||||
},
|
||||
notifyDockTilePlugin: {
|
||||
dockTileNotificationCount += 1
|
||||
}
|
||||
)
|
||||
|
||||
AppIconSettings.applyIcon(.dark, environment: environment)
|
||||
|
||||
XCTAssertTrue(receivedRuntimeIcon === expectedIcon)
|
||||
XCTAssertEqual(dockTileNotificationCount, 1)
|
||||
XCTAssertEqual(startObservationCallCount, 0)
|
||||
XCTAssertEqual(stopObservationCallCount, 1)
|
||||
}
|
||||
|
||||
func testApplyAutomaticStartsObservationAndNotifiesDockTilePlugin() {
|
||||
var dockTileNotificationCount = 0
|
||||
var startObservationCallCount = 0
|
||||
var stopObservationCallCount = 0
|
||||
|
||||
let environment = AppIconSettings.Environment(
|
||||
imageForMode: { mode in
|
||||
XCTFail("Automatic mode should not request a manual icon image: \(mode.rawValue)")
|
||||
return nil
|
||||
},
|
||||
setApplicationIconImage: { _ in
|
||||
XCTFail("Automatic mode should delegate live updates to the appearance observer")
|
||||
},
|
||||
startAppearanceObservation: {
|
||||
startObservationCallCount += 1
|
||||
},
|
||||
stopAppearanceObservation: {
|
||||
stopObservationCallCount += 1
|
||||
},
|
||||
notifyDockTilePlugin: {
|
||||
dockTileNotificationCount += 1
|
||||
}
|
||||
)
|
||||
|
||||
AppIconSettings.applyIcon(.automatic, environment: environment)
|
||||
|
||||
XCTAssertEqual(dockTileNotificationCount, 1)
|
||||
XCTAssertEqual(startObservationCallCount, 1)
|
||||
XCTAssertEqual(stopObservationCallCount, 0)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
final class NotificationDockBadgeTests: XCTestCase {
|
||||
private final class NotificationSettingsAlertSpy: NSAlert {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue