diff --git a/Xcodes.xcodeproj/project.pbxproj b/Xcodes.xcodeproj/project.pbxproj index cd61575..bf47b53 100644 --- a/Xcodes.xcodeproj/project.pbxproj +++ b/Xcodes.xcodeproj/project.pbxproj @@ -108,6 +108,7 @@ E8CBDB8927ADE32300B22292 /* unxip in Copy aria2c */ = {isa = PBXBuildFile; fileRef = E8CBDB8627ADD92000B22292 /* unxip */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; E8CBDB8B27AE02FF00B22292 /* ExperiementsPreferencePane.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8CBDB8A27AE02FF00B22292 /* ExperiementsPreferencePane.swift */; }; E8D0296F284B029800647641 /* BottomStatusBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8D0296E284B029800647641 /* BottomStatusBar.swift */; }; + E8D655C0288DD04700A139C2 /* SelectedActionType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8D655BF288DD04700A139C2 /* SelectedActionType.swift */; }; E8DA461125FAF7FB002E85EF /* NotificationsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8DA461025FAF7FB002E85EF /* NotificationsView.swift */; }; E8E98A9025D8631800EC89A0 /* InstallationStepRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAFBC3FF259AC17F00E2A3D8 /* InstallationStepRowView.swift */; }; E8E98A9625D863D700EC89A0 /* InstallationStepDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8E98A9525D863D700EC89A0 /* InstallationStepDetailView.swift */; }; @@ -296,6 +297,7 @@ E8CBDB8627ADD92000B22292 /* unxip */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.executable"; path = unxip; sourceTree = ""; }; E8CBDB8A27AE02FF00B22292 /* ExperiementsPreferencePane.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExperiementsPreferencePane.swift; sourceTree = ""; }; E8D0296E284B029800647641 /* BottomStatusBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottomStatusBar.swift; sourceTree = ""; }; + E8D655BF288DD04700A139C2 /* SelectedActionType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectedActionType.swift; sourceTree = ""; }; E8DA461025FAF7FB002E85EF /* NotificationsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsView.swift; sourceTree = ""; }; E8E98A9525D863D700EC89A0 /* InstallationStepDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstallationStepDetailView.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -479,6 +481,7 @@ CA11E7B92598476C00D2EE1C /* XcodeCommands.swift */, E87DD6EA25D053FA00D86808 /* Progress+.swift */, E81D7E9F2805250100A205FC /* Collection+.swift */, + E8D655BF288DD04700A139C2 /* SelectedActionType.swift */, ); path = Backend; sourceTree = ""; @@ -844,6 +847,7 @@ CAFE4AB425B7D3AF0064FE51 /* AdvancedPreferencePane.swift in Sources */, CA9FF84E2595079F00E47BAF /* ScrollingTextView.swift in Sources */, CABFA9C12592EEEA00380FEE /* Version+.swift in Sources */, + E8D655C0288DD04700A139C2 /* SelectedActionType.swift in Sources */, CA9FF8522595080100E47BAF /* AcknowledgementsView.swift in Sources */, CABFA9CE2592EEEA00380FEE /* Version+Xcode.swift in Sources */, CAFBDB912598FE80003DCC5A /* SelectedXcode.swift in Sources */, diff --git a/Xcodes/Backend/AppState.swift b/Xcodes/Backend/AppState.swift index 3fc9940..b13cff7 100644 --- a/Xcodes/Backend/AppState.swift +++ b/Xcodes/Backend/AppState.swift @@ -74,6 +74,20 @@ class AppState: ObservableObject { } } + var createSymLinkOnSelectDisabled: Bool { + return onSelectActionType == .rename + } + + @Published var onSelectActionType = SelectedActionType.none { + didSet { + Current.defaults.set(onSelectActionType.rawValue, forKey: "onSelectActionType") + + if onSelectActionType == .rename { + createSymLinkOnSelect = false + } + } + } + // MARK: - Publisher Cancellables var cancellables = Set() @@ -120,6 +134,7 @@ class AppState: ObservableObject { localPath = Current.defaults.string(forKey: "localPath") ?? Path.defaultXcodesApplicationSupport.string unxipExperiment = Current.defaults.bool(forKey: "unxipExperiment") ?? false createSymLinkOnSelect = Current.defaults.bool(forKey: "createSymLinkOnSelect") ?? false + onSelectActionType = SelectedActionType(rawValue: Current.defaults.string(forKey: "onSelectActionType") ?? "none") ?? .none } // MARK: Timer @@ -492,10 +507,15 @@ class AppState: ObservableObject { } guard - let installedXcodePath = xcode.installedPath, + var installedXcodePath = xcode.installedPath, selectPublisher == nil else { return } + if onSelectActionType == .rename { + guard let newDestinationXcodePath = renameToXcode(xcode: xcode) else { return } + installedXcodePath = newDestinationXcodePath + } + selectPublisher = installHelperIfNecessary() .flatMap { Current.helper.switchXcodePath(installedXcodePath.string) @@ -575,7 +595,39 @@ class AppState: ObservableObject { self.error = error self.presentedAlert = .generic(title: localizeString("Alert.SymLink.Title"), message: error.legibleLocalizedDescription) } + } + + func renameToXcode(xcode: Xcode) -> Path? { + guard let installedXcodePath = xcode.installedPath else { return nil } + let destinationPath: Path = Path.installDirectory/"Xcode.app" + + // rename any old named `Xcode.app` to the Xcodes versioned named files + if FileManager.default.fileExists(atPath: destinationPath.string) { + if let originalXcode = Current.files.installedXcode(destination: destinationPath) { + let newName = "Xcode-\(originalXcode.version.descriptionWithoutBuildMetadata).app" + Logger.appState.debug("Found Xcode.app - renaming back to \(newName)") + do { + try destinationPath.rename(to: newName) + } catch { + Logger.appState.error("Unable to create rename Xcode.app back to original") + self.error = error + // TODO UPDATE MY ERROR STRING + self.presentedAlert = .generic(title: localizeString("Alert.SymLink.Title"), message: error.legibleLocalizedDescription) + } + } + } + // rename passed in xcode to xcode.app + Logger.appState.debug("Found Xcode.app - renaming back to Xcode.app") + do { + return try installedXcodePath.rename(to: "Xcode.app") + } catch { + Logger.appState.error("Unable to create rename Xcode.app back to original") + self.error = error + // TODO UPDATE MY ERROR STRING + self.presentedAlert = .generic(title: localizeString("Alert.SymLink.Title"), message: error.legibleLocalizedDescription) + } + return nil } func updateAllXcodes(availableXcodes: [AvailableXcode], installedXcodes: [InstalledXcode], selectedXcodePath: String?) { diff --git a/Xcodes/Backend/Entry+.swift b/Xcodes/Backend/Entry+.swift index bdf85f7..c6e8c11 100644 --- a/Xcodes/Backend/Entry+.swift +++ b/Xcodes/Backend/Entry+.swift @@ -2,13 +2,12 @@ import Foundation import Path extension Entry { - var isAppBundle: Bool { + static func isAppBundle(kind: Kind, path: Path) -> Bool { kind == .directory && path.extension == "app" && !path.isSymlink } - - var infoPlist: InfoPlist? { + static func infoPlist(kind: Kind, path: Path) -> InfoPlist? { let infoPlistPath = path.join("Contents").join("Info.plist") guard let infoPlistData = try? Data(contentsOf: infoPlistPath.url), @@ -17,4 +16,12 @@ extension Entry { return infoPlist } + + var isAppBundle: Bool { + Entry.isAppBundle(kind: kind, path: path) + } + + var infoPlist: InfoPlist? { + Entry.infoPlist(kind: kind, path: path) + } } diff --git a/Xcodes/Backend/Environment.swift b/Xcodes/Backend/Environment.swift index de3b6b4..b94fbc7 100644 --- a/Xcodes/Backend/Environment.swift +++ b/Xcodes/Backend/Environment.swift @@ -164,7 +164,16 @@ public struct Files { } public var installedXcodes = _installedXcodes + + public func installedXcode(destination: Path) -> InstalledXcode? { + if Entry.isAppBundle(kind: destination.isDirectory ? .directory : .file, path: destination) && Entry.infoPlist(kind: destination.isDirectory ? .directory : .file, path: destination)?.bundleID == "com.apple.dt.Xcode" { + return InstalledXcode.init(path: destination) + } else { + return nil + } + } } + private func _installedXcodes(destination: Path) -> [InstalledXcode] { ((try? destination.ls()) ?? []) .filter { $0.isAppBundle && $0.infoPlist?.bundleID == "com.apple.dt.Xcode" } diff --git a/Xcodes/Backend/SelectedActionType.swift b/Xcodes/Backend/SelectedActionType.swift new file mode 100644 index 0000000..bce3a88 --- /dev/null +++ b/Xcodes/Backend/SelectedActionType.swift @@ -0,0 +1,31 @@ +// +// SelectedActionType.swift +// Xcodes +// +// Created by Matt Kiazyk on 2022-07-24. +// Copyright © 2022 Robots and Pencils. All rights reserved. +// + +import Foundation +public enum SelectedActionType: String, CaseIterable, Identifiable, CustomStringConvertible { + case none + case rename + + public var id: Self { self } + + public static var `default` = SelectedActionType.none + + public var description: String { + switch self { + case .none: return localizeString("OnSelectDoNothing") + case .rename: return localizeString("OnSelectRenameXcode") + } + } + + public var detailedDescription: String { + switch self { + case .none: return localizeString("OnSelectDoNothingDescription") + case .rename: return localizeString("OnSelectRenameXcodeDescription") + } + } +} diff --git a/Xcodes/Frontend/Preferences/AdvancedPreferencePane.swift b/Xcodes/Frontend/Preferences/AdvancedPreferencePane.swift index 890e1ca..3b86539 100644 --- a/Xcodes/Frontend/Preferences/AdvancedPreferencePane.swift +++ b/Xcodes/Frontend/Preferences/AdvancedPreferencePane.swift @@ -48,13 +48,27 @@ struct AdvancedPreferencePane: View { GroupBox(label: Text("Active/Select")) { VStack(alignment: .leading) { - Toggle( - "AutomaticallyCreateSymbolicLink", - isOn: $appState.createSymLinkOnSelect - ) - Text("AutomaticallyCreateSymbolicLinkDescription") + Picker("OnSelect", selection: $appState.onSelectActionType) { + + Text(SelectedActionType.none.description) + .tag(SelectedActionType.none) + Text(SelectedActionType.rename.description) + .tag(SelectedActionType.rename) + } + .labelsHidden() + .pickerStyle(.inline) + + Text(appState.onSelectActionType.detailedDescription) .font(.footnote) .fixedSize(horizontal: false, vertical: true) + Spacer() + .frame(height: 20) + + Toggle("AutomaticallyCreateSymbolicLink", isOn: $appState.createSymLinkOnSelect) + .disabled(appState.createSymLinkOnSelectDisabled) + Text("AutomaticallyCreateSymbolicLinkDescription") + .font(.footnote) + .fixedSize(horizontal: false, vertical: true) } .fixedSize(horizontal: false, vertical: true) } @@ -154,6 +168,7 @@ struct AdvancedPreferencePane_Previews: PreviewProvider { .environmentObject(AppState()) .frame(maxWidth: 500) } + .frame(width: 500, height: 700, alignment: .center) } } diff --git a/Xcodes/Resources/en.lproj/Localizable.strings b/Xcodes/Resources/en.lproj/Localizable.strings index 21286a9..d304463 100644 --- a/Xcodes/Resources/en.lproj/Localizable.strings +++ b/Xcodes/Resources/en.lproj/Localizable.strings @@ -78,8 +78,14 @@ "LocalCachePathDescription" = "Xcodes caches available Xcode versions and temporary downloads new versions to a directory"; "Change" = "Change"; "Active/Select" = "Active/Select"; + +"OnSelectDoNothing" = "Keep name as Xcode-X.X.X.app"; +"OnSelectDoNothingDescription" = "On select, will keep the name as the version eg. Xcode-13.4.1.app"; "AutomaticallyCreateSymbolicLink" = "Automatically create symbolic link to Xcode.app"; "AutomaticallyCreateSymbolicLinkDescription" = "When making an Xcode version Active/Selected, try and create a symbolic link named Xcode.app in the installation directory"; +"OnSelectRenameXcode" = "Always rename to Xcode.app"; +"OnSelectRenameXcodeDescription" = "On select, will automatically try and rename the active Xcode to Xcode.app, renaming the previous Xcode.app to the version name."; + "DataSource" = "Data Source"; "DataSourceDescription" = "The Apple data source scrapes the Apple Developer website. It will always show the latest releases that are available, but is more fragile.\n\nXcode Releases is an unofficial list of Xcode releases. It's provided as well-formed data, contains extra information that is not readily available from Apple, and is less likely to break if Apple redesigns their developer website."; "Downloader" = "Downloader";