mirror of
https://github.com/XcodesOrg/XcodesApp.git
synced 2026-04-27 15:07:39 +00:00
Reflect currently-selected Xcode in list
This commit is contained in:
parent
e3f07e855e
commit
047288384d
6 changed files with 89 additions and 17 deletions
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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() {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}())
|
}())
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue