Reflect currently-selected Xcode in list

This commit is contained in:
Brandon Evans 2020-12-28 10:35:06 -07:00
parent e3f07e855e
commit 047288384d
No known key found for this signature in database
GPG key ID: D58A4B8DB64F8E93
6 changed files with 89 additions and 17 deletions

View file

@ -15,13 +15,25 @@ extension AppState {
let lastUpdated = Current.defaults.date(forKey: "lastUpdated"), let lastUpdated = Current.defaults.date(forKey: "lastUpdated"),
// This is bad date math but for this use case it doesn't need to be exact // This is bad date math but for this use case it doesn't need to be exact
lastUpdated < Current.date().addingTimeInterval(-60 * 60 * 24) lastUpdated < Current.date().addingTimeInterval(-60 * 60 * 24)
else { return } else {
updatePublisher = updateSelectedXcodePath()
.sink(
receiveCompletion: { _ in
self.updatePublisher = nil
},
receiveValue: { _ in }
)
return
}
update() as Void update() as Void
} }
func update() { func update() {
guard !isUpdating else { return } guard !isUpdating else { return }
updatePublisher = updateAvailableXcodes(from: self.dataSource) updatePublisher = updateSelectedXcodePath()
.flatMap { _ in
self.updateAvailableXcodes(from: self.dataSource)
}
.sink( .sink(
receiveCompletion: { [unowned self] completion in receiveCompletion: { [unowned self] completion in
switch completion { switch completion {
@ -36,6 +48,15 @@ extension AppState {
receiveValue: { _ in } receiveValue: { _ in }
) )
} }
func updateSelectedXcodePath() -> AnyPublisher<Void, Never> {
Current.shell.xcodeSelectPrintPath()
.handleEvents(receiveOutput: { output in self.selectedXcodePath = output.out })
// Ignore xcode-select failures
.map { _ in Void() }
.catch { _ in Just(()) }
.eraseToAnyPublisher()
}
private func updateAvailableXcodes(from dataSource: DataSource) -> AnyPublisher<[AvailableXcode], Error> { private func updateAvailableXcodes(from dataSource: DataSource) -> AnyPublisher<[AvailableXcode], Error> {
switch dataSource { switch dataSource {

View file

@ -16,10 +16,15 @@ class AppState: ObservableObject {
@Published var authenticationState: AuthenticationState = .unauthenticated @Published var authenticationState: AuthenticationState = .unauthenticated
@Published var availableXcodes: [AvailableXcode] = [] { @Published var availableXcodes: [AvailableXcode] = [] {
willSet { willSet {
updateAllXcodes(newValue) updateAllXcodes(availableXcodes: newValue, selectedXcodePath: selectedXcodePath)
} }
} }
var allXcodes: [Xcode] = [] var allXcodes: [Xcode] = []
@Published var selectedXcodePath: String? {
willSet {
updateAllXcodes(availableXcodes: availableXcodes, selectedXcodePath: newValue)
}
}
@Published var updatePublisher: AnyCancellable? @Published var updatePublisher: AnyCancellable?
var isUpdating: Bool { updatePublisher != nil } var isUpdating: Bool { updatePublisher != nil }
@Published var error: AlertContent? @Published var error: AlertContent?
@ -226,6 +231,9 @@ class AppState: ObservableObject {
else { return } else { return }
selectPublisher = HelperClient().switchXcodePath(installedXcode.path.string) selectPublisher = HelperClient().switchXcodePath(installedXcode.path.string)
.flatMap { [unowned self] _ in
self.updateSelectedXcodePath()
}
.sink( .sink(
receiveCompletion: { [unowned self] completion in receiveCompletion: { [unowned self] completion in
if case let .failure(error) = completion { if case let .failure(error) = completion {
@ -251,9 +259,9 @@ class AppState: ObservableObject {
// MARK: - Private // MARK: - Private
private func updateAllXcodes(_ xcodes: [AvailableXcode]) { private func updateAllXcodes(availableXcodes: [AvailableXcode], selectedXcodePath: String?) {
let installedXcodes = Current.files.installedXcodes(Path.root/"Applications") let installedXcodes = Current.files.installedXcodes(Path.root/"Applications")
var allXcodeVersions = xcodes.map { $0.version } var allXcodeVersions = availableXcodes.map { $0.version }
for installedXcode in installedXcodes { for installedXcode in installedXcodes {
// If an installed version isn't listed online, add the installed version // If an installed version isn't listed online, add the installed version
if !allXcodeVersions.contains(where: { version in if !allXcodeVersions.contains(where: { version in
@ -274,11 +282,11 @@ class AppState: ObservableObject {
.sorted(by: >) .sorted(by: >)
.map { xcodeVersion in .map { xcodeVersion in
let installedXcode = installedXcodes.first(where: { xcodeVersion.isEquivalentForDeterminingIfInstalled(toInstalled: $0.version) }) let installedXcode = installedXcodes.first(where: { xcodeVersion.isEquivalentForDeterminingIfInstalled(toInstalled: $0.version) })
let availableXcode = xcodes.first { $0.version == xcodeVersion } let availableXcode = availableXcodes.first { $0.version == xcodeVersion }
return Xcode( return Xcode(
version: xcodeVersion, version: xcodeVersion,
installState: installedXcodes.contains(where: { xcodeVersion.isEquivalentForDeterminingIfInstalled(toInstalled: $0.version) }) ? .installed : .notInstalled, installState: installedXcode != nil ? .installed : .notInstalled,
selected: false, selected: installedXcode != nil && selectedXcodePath?.hasPrefix(installedXcode!.path.string) == true,
path: installedXcode?.path.string, path: installedXcode?.path.string,
icon: (installedXcode?.path.string).map(NSWorkspace.shared.icon(forFile:)), icon: (installedXcode?.path.string).map(NSWorkspace.shared.icon(forFile:)),
requiredMacOSVersion: availableXcode?.requiredMacOSVersion, requiredMacOSVersion: availableXcode?.requiredMacOSVersion,

View file

@ -1,3 +1,4 @@
import Combine
import Foundation import Foundation
import PromiseKit import PromiseKit
import PMKFoundation import PMKFoundation
@ -50,11 +51,7 @@ public struct Shell {
authenticateSudoerIfNecessary(passwordInput) authenticateSudoerIfNecessary(passwordInput)
} }
public var xcodeSelectPrintPath: () -> Promise<ProcessOutput> = { Process.run(Path.root.usr.bin.join("xcode-select"), "-p") } public var xcodeSelectPrintPath: () -> AnyPublisher<ProcessOutput, Error> = { Process.run(Path.root.usr.bin.join("xcode-select"), "-p") }
public var xcodeSelectSwitch: (String?, String) -> Promise<ProcessOutput> = { Process.sudo(password: $0, Path.root.usr.bin.join("xcode-select"), "-s", $1) }
public func xcodeSelectSwitch(password: String?, path: String) -> Promise<ProcessOutput> {
xcodeSelectSwitch(password, path)
}
} }
public struct Files { public struct Files {

View file

@ -1,3 +1,4 @@
import Combine
import Foundation import Foundation
import PromiseKit import PromiseKit
import PMKFoundation import PMKFoundation
@ -38,4 +39,47 @@ extension Process {
return (process.terminationStatus, output, error) return (process.terminationStatus, output, error)
} }
} }
@discardableResult
static func run(_ executable: Path, workingDirectory: URL? = nil, input: String? = nil, _ arguments: String...) -> AnyPublisher<ProcessOutput, Error> {
return run(executable.url, workingDirectory: workingDirectory, input: input, arguments)
}
@discardableResult
static func run(_ executable: URL, workingDirectory: URL? = nil, input: String? = nil, _ arguments: [String]) -> AnyPublisher<ProcessOutput, Error> {
Deferred {
Future<ProcessOutput, Error> { promise in
let process = Process()
process.currentDirectoryURL = workingDirectory ?? executable.deletingLastPathComponent()
process.executableURL = executable
process.arguments = arguments
let (stdout, stderr) = (Pipe(), Pipe())
process.standardOutput = stdout
process.standardError = stderr
if let input = input {
let inputPipe = Pipe()
process.standardInput = inputPipe.fileHandleForReading
inputPipe.fileHandleForWriting.write(Data(input.utf8))
inputPipe.fileHandleForWriting.closeFile()
}
do {
try process.run()
process.waitUntilExit()
let output = String(data: stdout.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) ?? ""
let error = String(data: stderr.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) ?? ""
promise(.success((process.terminationStatus, output, error)))
} catch {
promise(.failure(error))
}
}
}
.subscribe(on: DispatchQueue.global())
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
} }

View file

@ -58,6 +58,7 @@ struct SelectButton: View {
Button(action: select) { Button(action: select) {
Text("Select") Text("Select")
} }
.disabled(xcode?.selected != false)
} }
private func select() { private func select() {

View file

@ -42,13 +42,14 @@ struct XcodeListView: View {
.foregroundColor(appState.selectedXcodeID == xcode.id ? Color(NSColor.selectedMenuItemTextColor) : Color(NSColor.secondaryLabelColor)) .foregroundColor(appState.selectedXcodeID == xcode.id ? Color(NSColor.selectedMenuItemTextColor) : Color(NSColor.secondaryLabelColor))
} }
Spacer()
if xcode.selected { if xcode.selected {
Tag(text: "SELECTED") Tag(text: "SELECTED")
.foregroundColor(.green) .foregroundColor(.green)
} }
Spacer()
Button(xcode.installed ? "INSTALLED" : "INSTALL") { Button(xcode.installed ? "INSTALLED" : "INSTALL") {
print("Installing...") print("Installing...")
} }
@ -90,10 +91,10 @@ struct XcodeListView_Previews: PreviewProvider {
.environmentObject({ () -> AppState in .environmentObject({ () -> AppState in
let a = AppState() let a = AppState()
a.allXcodes = [ a.allXcodes = [
Xcode(version: Version("12.3.0")!, installState: .installed, selected: true, path: nil, icon: nil), Xcode(version: Version("12.3.0")!, installState: .installed, selected: true, path: "/Applications/Xcode-12.3.0.app", icon: nil),
Xcode(version: Version("12.2.0")!, installState: .notInstalled, selected: false, path: nil, icon: nil), Xcode(version: Version("12.2.0")!, installState: .notInstalled, selected: false, path: nil, icon: nil),
Xcode(version: Version("12.1.0")!, installState: .notInstalled, selected: false, path: nil, icon: nil), Xcode(version: Version("12.1.0")!, installState: .notInstalled, selected: false, path: nil, icon: nil),
Xcode(version: Version("12.0.0")!, installState: .installed, selected: false, path: nil, icon: nil), Xcode(version: Version("12.0.0")!, installState: .installed, selected: false, path: "/Applications/Xcode-12.3.0.app", icon: nil),
] ]
return a return a
}()) }())