Uninstall a xcode version

This commit is contained in:
Matt Kiazyk 2020-12-31 12:36:31 -06:00
parent 38756100b7
commit 7bfb94d75a
No known key found for this signature in database
GPG key ID: 967DBC53389132D7
5 changed files with 117 additions and 23 deletions

View file

@ -12,6 +12,7 @@ class AppState: ObservableObject {
private let helperClient = HelperClient() private let helperClient = HelperClient()
private var cancellables = Set<AnyCancellable>() private var cancellables = Set<AnyCancellable>()
private var selectPublisher: AnyCancellable? private var selectPublisher: AnyCancellable?
private var uninstallPublisher: AnyCancellable?
@Published var authenticationState: AuthenticationState = .unauthenticated @Published var authenticationState: AuthenticationState = .unauthenticated
@Published var availableXcodes: [AvailableXcode] = [] { @Published var availableXcodes: [AvailableXcode] = [] {
@ -32,7 +33,6 @@ class AppState: ObservableObject {
@Published var presentingSignInAlert = false @Published var presentingSignInAlert = false
@Published var isProcessingAuthRequest = false @Published var isProcessingAuthRequest = false
@Published var secondFactorData: SecondFactorData? @Published var secondFactorData: SecondFactorData?
@Published var xcodeBeingConfirmedForUninstallation: Xcode?
@Published var helperInstallState: HelperInstallState = .notInstalled @Published var helperInstallState: HelperInstallState = .notInstalled
init() { init() {
@ -200,14 +200,36 @@ class AppState: ObservableObject {
.store(in: &cancellables) .store(in: &cancellables)
} }
// MARK: - // MARK: - Install
func install(id: Xcode.ID) { func install(id: Xcode.ID) {
// TODO: // TODO:
} }
// MARK: - Uninstall
func uninstall(id: Xcode.ID) { func uninstall(id: Xcode.ID) {
// TODO: if helperInstallState == .notInstalled {
installHelper()
}
guard
let installedXcode = Current.files.installedXcodes(Path.root/"Applications").first(where: { $0.version == id }),
uninstallPublisher == nil
else { return }
uninstallPublisher = HelperClient().uninstallXcode(installedXcode.path)
.flatMap { [unowned self] _ in
self.updateSelectedXcodePath()
}
.sink(
receiveCompletion: { [unowned self] completion in
if case let .failure(error) = completion {
self.error = AlertContent(title: "Error uninstalling Xcode", message: error.legibleLocalizedDescription)
}
self.uninstallPublisher = nil
},
receiveValue: { _ in }
)
} }
func reveal(id: Xcode.ID) { func reveal(id: Xcode.ID) {

View file

@ -1,5 +1,6 @@
import Combine import Combine
import Foundation import Foundation
import Path
final class HelperClient { final class HelperClient {
private var connection: NSXPCConnection? private var connection: NSXPCConnection?
@ -103,4 +104,27 @@ final class HelperClient {
.map { $0.0 } .map { $0.0 }
.eraseToAnyPublisher() .eraseToAnyPublisher()
} }
func uninstallXcode(_ path: Path) -> AnyPublisher<Void, Error> {
let connectionErrorSubject = PassthroughSubject<String, Error>()
return Deferred {
Future { promise in
do {
try Current.files.trashItem(at: path.url)
promise(.success(()))
} catch {
promise(.failure(error))
}
}
}
// Take values, but fail when connectionErrorSubject fails
.zip(
connectionErrorSubject
.prepend("")
.map { _ in Void() }
)
.map { $0.0 }
.eraseToAnyPublisher()
}
} }

View file

@ -9,6 +9,7 @@ struct XcodeCommands: Commands {
var body: some Commands { var body: some Commands {
CommandMenu("Xcode") { CommandMenu("Xcode") {
Group { Group {
InstallCommand() InstallCommand()
Divider() Divider()
@ -17,6 +18,7 @@ struct XcodeCommands: Commands {
OpenCommand() OpenCommand()
RevealCommand() RevealCommand()
CopyPathCommand() CopyPathCommand()
UninstallCommand()
} }
.environmentObject(appState) .environmentObject(appState)
} }
@ -31,24 +33,15 @@ struct InstallButton: View {
let xcode: Xcode? let xcode: Xcode?
var body: some View { var body: some View {
Button(action: uninstallOrInstall) { Button(action: install) {
if let xcode = xcode { Text("Install")
Text(xcode.installed == true ? "Uninstall" : "Install") .help("Install")
.help(xcode.installed == true ? "Uninstall" : "Install")
} else {
Text("Install")
.help("Install")
}
} }
} }
private func uninstallOrInstall() { private func install() {
guard let xcode = xcode else { return } guard let xcode = xcode else { return }
if xcode.installed { appState.install(id: xcode.id)
appState.xcodeBeingConfirmedForUninstallation = xcode
} else {
appState.install(id: xcode.id)
}
} }
} }
@ -91,6 +84,30 @@ struct OpenButton: View {
} }
} }
struct UninstallButton: View {
@EnvironmentObject var appState: AppState
let xcode: Xcode?
@State private var showingAlert = false
var alert: Alert {
Alert(title: Text("Uninstall Xcode \(xcode!.description)?"),
message: Text("It will be moved to the Trash, but won't be emptied."),
primaryButton: .destructive(Text("Uninstall"), action: { self.appState.uninstall(id: xcode!.id) }),
secondaryButton: .cancel(Text("Cancel")))
}
var body: some View {
Button(action: {
self.showingAlert = true
}) {
Text("Uninstall")
}
.foregroundColor(.red)
.help("Uninstall")
.alert(isPresented:$showingAlert, content: { self.alert })
}
}
struct RevealButton: View { struct RevealButton: View {
@EnvironmentObject var appState: AppState @EnvironmentObject var appState: AppState
let xcode: Xcode? let xcode: Xcode?
@ -133,8 +150,8 @@ struct InstallCommand: View {
var body: some View { var body: some View {
InstallButton(xcode: selectedXcode.unwrapped) InstallButton(xcode: selectedXcode.unwrapped)
.keyboardShortcut(selectedXcode.unwrapped?.installed == true ? "u" : "i", modifiers: [.command, .option]) .keyboardShortcut("i", modifiers: [.command, .option])
.disabled(selectedXcode.unwrapped == nil) .disabled(selectedXcode.unwrapped?.installed == true)
} }
} }
@ -181,3 +198,14 @@ struct CopyPathCommand: View {
.disabled(selectedXcode.unwrapped?.installed != true) .disabled(selectedXcode.unwrapped?.installed != true)
} }
} }
struct UninstallCommand: View {
@EnvironmentObject var appState: AppState
@FocusedValue(\.selectedXcode) private var selectedXcode: SelectedXcode?
var body: some View {
UninstallButton(xcode: selectedXcode.unwrapped)
.keyboardShortcut("u", modifiers: [.command, .option])
.disabled(selectedXcode.unwrapped?.installed != true)
}
}

View file

@ -50,6 +50,11 @@ struct InfoPane: View {
sdks(for: xcode) sdks(for: xcode)
compilers(for: xcode) compilers(for: xcode)
if xcode.path != nil {
VStack(alignment: .leading) {
UninstallButton(xcode: xcode)
}
}
Spacer() Spacer()
} }
} else { } else {
@ -250,6 +255,21 @@ struct InfoPane_Previews: PreviewProvider {
}) })
.previewDisplayName("Populated, Uninstalled") .previewDisplayName("Populated, Uninstalled")
InfoPane(selectedXcodeID: Version(major: 12, minor: 3, patch: 0))
.environmentObject(configure(AppState()) {
$0.allXcodes = [
.init(
version: Version(major: 12, minor: 3, patch: 0),
installState: .installed,
selected: false,
path: "/Applications/Xcode-12.3.0.app",
icon: nil,
sdks: nil,
compilers: nil)
]
})
.previewDisplayName("Basic, installed")
InfoPane(selectedXcodeID: nil) InfoPane(selectedXcodeID: nil)
.environmentObject(configure(AppState()) { .environmentObject(configure(AppState()) {
$0.allXcodes = [ $0.allXcodes = [

View file

@ -26,15 +26,15 @@ struct XcodeListViewRow: View {
installControl(for: xcode) installControl(for: xcode)
} }
.contextMenu { .contextMenu {
InstallButton(xcode: xcode)
Divider()
if xcode.installed { if xcode.installed {
SelectButton(xcode: xcode) SelectButton(xcode: xcode)
OpenButton(xcode: xcode) OpenButton(xcode: xcode)
RevealButton(xcode: xcode) RevealButton(xcode: xcode)
CopyPathButton(xcode: xcode) CopyPathButton(xcode: xcode)
Divider()
UninstallButton(xcode: xcode)
} else {
InstallButton(xcode: xcode)
} }
} }
} }