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 var cancellables = Set<AnyCancellable>()
private var selectPublisher: AnyCancellable?
private var uninstallPublisher: AnyCancellable?
@Published var authenticationState: AuthenticationState = .unauthenticated
@Published var availableXcodes: [AvailableXcode] = [] {
@ -32,7 +33,6 @@ class AppState: ObservableObject {
@Published var presentingSignInAlert = false
@Published var isProcessingAuthRequest = false
@Published var secondFactorData: SecondFactorData?
@Published var xcodeBeingConfirmedForUninstallation: Xcode?
@Published var helperInstallState: HelperInstallState = .notInstalled
init() {
@ -200,14 +200,36 @@ class AppState: ObservableObject {
.store(in: &cancellables)
}
// MARK: -
// MARK: - Install
func install(id: Xcode.ID) {
// TODO:
}
// MARK: - Uninstall
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) {

View file

@ -1,5 +1,6 @@
import Combine
import Foundation
import Path
final class HelperClient {
private var connection: NSXPCConnection?
@ -103,4 +104,27 @@ final class HelperClient {
.map { $0.0 }
.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 {
CommandMenu("Xcode") {
Group {
InstallCommand()
Divider()
@ -17,6 +18,7 @@ struct XcodeCommands: Commands {
OpenCommand()
RevealCommand()
CopyPathCommand()
UninstallCommand()
}
.environmentObject(appState)
}
@ -31,24 +33,15 @@ struct InstallButton: View {
let xcode: Xcode?
var body: some View {
Button(action: uninstallOrInstall) {
if let xcode = xcode {
Text(xcode.installed == true ? "Uninstall" : "Install")
.help(xcode.installed == true ? "Uninstall" : "Install")
} else {
Text("Install")
.help("Install")
}
Button(action: install) {
Text("Install")
.help("Install")
}
}
private func uninstallOrInstall() {
private func install() {
guard let xcode = xcode else { return }
if xcode.installed {
appState.xcodeBeingConfirmedForUninstallation = xcode
} else {
appState.install(id: xcode.id)
}
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 {
@EnvironmentObject var appState: AppState
let xcode: Xcode?
@ -133,8 +150,8 @@ struct InstallCommand: View {
var body: some View {
InstallButton(xcode: selectedXcode.unwrapped)
.keyboardShortcut(selectedXcode.unwrapped?.installed == true ? "u" : "i", modifiers: [.command, .option])
.disabled(selectedXcode.unwrapped == nil)
.keyboardShortcut("i", modifiers: [.command, .option])
.disabled(selectedXcode.unwrapped?.installed == true)
}
}
@ -181,3 +198,14 @@ struct CopyPathCommand: View {
.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)
compilers(for: xcode)
if xcode.path != nil {
VStack(alignment: .leading) {
UninstallButton(xcode: xcode)
}
}
Spacer()
}
} else {
@ -250,6 +255,21 @@ struct InfoPane_Previews: PreviewProvider {
})
.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)
.environmentObject(configure(AppState()) {
$0.allXcodes = [

View file

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