From c675a98309bfb29c3f9f345d659151c9faf52117 Mon Sep 17 00:00:00 2001 From: cdricms <36056008+cdricms@users.noreply.github.com> Date: Tue, 9 Jul 2024 20:29:20 +0200 Subject: [PATCH] Better reactivity and functionality --- Brewer.xcodeproj/project.pbxproj | 28 ++ Brewer/BrewerApp.swift | 36 ++- Brewer/CaskDetailView.swift | 38 +++ Brewer/Components/DownloadButton.swift | 53 ++++ Brewer/Components/UninstallButton.swift | 45 +++ Brewer/ContentView.swift | 39 ++- Brewer/InstalledView.swift | 205 +++++++------- Brewer/Model/Homebrew.swift | 348 +++++++++++++----------- Brewer/SearchView.swift | 110 ++++---- 9 files changed, 541 insertions(+), 361 deletions(-) create mode 100644 Brewer/CaskDetailView.swift create mode 100644 Brewer/Components/DownloadButton.swift create mode 100644 Brewer/Components/UninstallButton.swift diff --git a/Brewer.xcodeproj/project.pbxproj b/Brewer.xcodeproj/project.pbxproj index f5ea0fe..e83269a 100644 --- a/Brewer.xcodeproj/project.pbxproj +++ b/Brewer.xcodeproj/project.pbxproj @@ -9,6 +9,9 @@ /* Begin PBXBuildFile section */ 9E6C73072C3D5E570056ADDC /* SearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E6C73062C3D5E570056ADDC /* SearchView.swift */; }; 9E6C73092C3D5E950056ADDC /* InstalledView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E6C73082C3D5E950056ADDC /* InstalledView.swift */; }; + 9E6C730C2C3D796D0056ADDC /* DownloadButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E6C730B2C3D796D0056ADDC /* DownloadButton.swift */; }; + 9E6C730E2C3DB16F0056ADDC /* UninstallButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E6C730D2C3DB16F0056ADDC /* UninstallButton.swift */; }; + 9E6C73112C3DB5940056ADDC /* CaskDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E6C73102C3DB5940056ADDC /* CaskDetailView.swift */; }; 9E8CE5362C3C545600A39146 /* BrewerApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E8CE5352C3C545600A39146 /* BrewerApp.swift */; }; 9E8CE5382C3C545600A39146 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E8CE5372C3C545600A39146 /* ContentView.swift */; }; 9E8CE53A2C3C545700A39146 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9E8CE5392C3C545700A39146 /* Assets.xcassets */; }; @@ -39,6 +42,9 @@ /* Begin PBXFileReference section */ 9E6C73062C3D5E570056ADDC /* SearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchView.swift; sourceTree = ""; }; 9E6C73082C3D5E950056ADDC /* InstalledView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstalledView.swift; sourceTree = ""; }; + 9E6C730B2C3D796D0056ADDC /* DownloadButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadButton.swift; sourceTree = ""; }; + 9E6C730D2C3DB16F0056ADDC /* UninstallButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UninstallButton.swift; sourceTree = ""; }; + 9E6C73102C3DB5940056ADDC /* CaskDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CaskDetailView.swift; sourceTree = ""; }; 9E8CE5322C3C545600A39146 /* Brewer.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Brewer.app; sourceTree = BUILT_PRODUCTS_DIR; }; 9E8CE5352C3C545600A39146 /* BrewerApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrewerApp.swift; sourceTree = ""; }; 9E8CE5372C3C545600A39146 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; @@ -78,6 +84,23 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 9E6C730A2C3D795D0056ADDC /* Components */ = { + isa = PBXGroup; + children = ( + 9E6C730B2C3D796D0056ADDC /* DownloadButton.swift */, + 9E6C730D2C3DB16F0056ADDC /* UninstallButton.swift */, + ); + path = Components; + sourceTree = ""; + }; + 9E6C730F2C3DB5850056ADDC /* Details */ = { + isa = PBXGroup; + children = ( + 9E6C73102C3DB5940056ADDC /* CaskDetailView.swift */, + ); + name = Details; + sourceTree = ""; + }; 9E8CE5292C3C545600A39146 = { isa = PBXGroup; children = ( @@ -101,9 +124,11 @@ 9E8CE5342C3C545600A39146 /* Brewer */ = { isa = PBXGroup; children = ( + 9E6C730A2C3D795D0056ADDC /* Components */, 9E8CE5602C3C5A5000A39146 /* Model */, 9E8CE5352C3C545600A39146 /* BrewerApp.swift */, 9E6C73062C3D5E570056ADDC /* SearchView.swift */, + 9E6C730F2C3DB5850056ADDC /* Details */, 9E6C73082C3D5E950056ADDC /* InstalledView.swift */, 9E8CE5372C3C545600A39146 /* ContentView.swift */, 9E8CE5392C3C545700A39146 /* Assets.xcassets */, @@ -277,10 +302,13 @@ buildActionMask = 2147483647; files = ( 9E6C73072C3D5E570056ADDC /* SearchView.swift in Sources */, + 9E6C730C2C3D796D0056ADDC /* DownloadButton.swift in Sources */, 9E8CE5382C3C545600A39146 /* ContentView.swift in Sources */, + 9E6C730E2C3DB16F0056ADDC /* UninstallButton.swift in Sources */, 9E8CE5622C3C5A6A00A39146 /* Homebrew.swift in Sources */, 9E6C73092C3D5E950056ADDC /* InstalledView.swift in Sources */, 9E8CE5362C3C545600A39146 /* BrewerApp.swift in Sources */, + 9E6C73112C3DB5940056ADDC /* CaskDetailView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Brewer/BrewerApp.swift b/Brewer/BrewerApp.swift index 23531ce..4addcd1 100644 --- a/Brewer/BrewerApp.swift +++ b/Brewer/BrewerApp.swift @@ -9,12 +9,34 @@ import SwiftUI @main struct BrewerApp: App { - var body: some Scene { - WindowGroup { - ContentView() - } - MenuBarExtra("Brewer", systemImage: "sparkle.magnifyingglass") { - ContentView().padding() - }.menuBarExtraStyle(.window) + @AppStorage("isUpToDate") var isUpToDate: Bool = true + init() { + do { + let res = try shell("/opt/homebrew/bin/brew outdated --greedy-latest -g | wc -l") + if let number = Int(res.trimmingCharacters(in: .whitespacesAndNewlines)), number > 0 { + isUpToDate = false + } + } catch { + print(error.localizedDescription) } + } + + var body: some Scene { + WindowGroup { + EmptyView() + } + MenuBarExtra { + ContentView().padding() + } label: { + if isUpToDate { + Label("Brewer", systemImage: "sparkle.magnifyingglass") + } else { + Label("Brewer", systemImage: "sparkle.magnifyingglass") + .symbolRenderingMode(.palette) + .foregroundStyle(.orange) + } + } + .menuBarExtraStyle(.window) + } + } diff --git a/Brewer/CaskDetailView.swift b/Brewer/CaskDetailView.swift new file mode 100644 index 0000000..7661c47 --- /dev/null +++ b/Brewer/CaskDetailView.swift @@ -0,0 +1,38 @@ +// +// CaskDetailView.swift +// Brewer +// +// Created by Cédric MAS on 09/07/2024. +// + +import SwiftUI + +struct CaskDetailView: View { + var cask: Cask + @Bindable var brewListing: Homebrew + var body: some View { + NavigationStack { + VStack { + Text("Identifier: \(cask.fullToken)") + Text("Version: \(cask.version)") + } + .navigationTitle(cask.name.first ?? cask.fullToken) + .toolbar { + ToolbarItem(placement: .primaryAction) { + if cask.installed == nil { + DownloadButton(name: cask.fullToken, isCask: true) + } else if cask.outdated { + Button("Update") { + + } + } + } + if cask.installed != nil { + ToolbarItem(placement: .primaryAction) { + UninstallButton(name: cask.fullToken, brewListing: brewListing) + } + } + } + } + } +} diff --git a/Brewer/Components/DownloadButton.swift b/Brewer/Components/DownloadButton.swift new file mode 100644 index 0000000..6394846 --- /dev/null +++ b/Brewer/Components/DownloadButton.swift @@ -0,0 +1,53 @@ +// +// DownloadButton.swift +// Brewer +// +// Created by Cédric MAS on 09/07/2024. +// + +import SwiftUI + +struct DownloadButton: View { + let name: String + var isCask: Bool = false + @State private var brew = Homebrew() + @State private var downloaded = false + + var body: some View { + VStack { + if downloaded { + Image(systemName: "checkmark") + .symbolRenderingMode(.palette) + .symbolVariant(.circle) + .symbolVariant(.fill) + .foregroundStyle(.white, .green) + } else if brew.isLoading { + ProgressView() + .controlSize(.small) + } else if brew.errorMessage != nil { + Image(systemName: "x") + .symbolRenderingMode(.palette) + .symbolVariant(.circle) + .symbolVariant(.fill) + .foregroundStyle(.white, .red) + } else { + Button("Get") { + Task { + downloaded = await brew.install(name, isCask: isCask) + } + } + .buttonBorderShape(.capsule) + .buttonStyle(.borderedProminent) + } + } + .onAppear { + Task { + downloaded = await brew.isDownloaded(name) + } + } + } +} + +#Preview { + DownloadButton(name: "firefox") +} diff --git a/Brewer/Components/UninstallButton.swift b/Brewer/Components/UninstallButton.swift new file mode 100644 index 0000000..aaf6bbf --- /dev/null +++ b/Brewer/Components/UninstallButton.swift @@ -0,0 +1,45 @@ +// +// UninstallButton.swift +// Brewer +// +// Created by Cédric MAS on 09/07/2024. +// + +import SwiftUI + +struct UninstallButton: View { + let name: String + @State private var brew = Homebrew() + @State private var uninstalled = false + @Bindable var brewListing: Homebrew + var body: some View { + VStack { + if uninstalled { + EmptyView() + } else if brew.isLoading { + ProgressView() + .controlSize(.small) + } else if brew.errorMessage != nil { + Image(systemName: "x") + .symbolRenderingMode(.palette) + .symbolVariant(.circle) + .symbolVariant(.fill) + .foregroundStyle(.white, .red) + } else { + Button("Uninstall", role: .destructive) { + Task { + uninstalled = await brew.uninstall(name) + brewListing.getInstalled() + } + } + .buttonBorderShape(.capsule) + .buttonStyle(.borderedProminent) + } + } + .onAppear { + Task { + uninstalled = await brew.isDownloaded(name) + } + } + } +} diff --git a/Brewer/ContentView.swift b/Brewer/ContentView.swift index cdecd64..3208d4a 100644 --- a/Brewer/ContentView.swift +++ b/Brewer/ContentView.swift @@ -8,30 +8,29 @@ import SwiftUI enum SegementedSelection: String, CaseIterable { - case search = "Search" - case installed = "Installed" + case search = "Search" + case installed = "Installed" } struct ContentView: View { - @State private var segmentedSelection: SegementedSelection = .search - var body: some View { - NavigationStack { - - Picker("", selection: $segmentedSelection) { - ForEach(SegementedSelection.allCases, id: \.self) { sel in - Text(sel.rawValue).tag(sel) - } - }.pickerStyle(.segmented) - switch segmentedSelection { - case .search: - SearchView() - case .installed: - InstalledView() - } - } - } + @State private var segmentedSelection: SegementedSelection = .search + var body: some View { + NavigationStack { + Picker("", selection: $segmentedSelection) { + ForEach(SegementedSelection.allCases, id: \.self) { sel in + Text(sel.rawValue).tag(sel) + } + }.pickerStyle(.segmented) + switch segmentedSelection { + case .search: + SearchView() + case .installed: + InstalledView() + } + } + } } #Preview { - ContentView() + ContentView() } diff --git a/Brewer/InstalledView.swift b/Brewer/InstalledView.swift index aea280c..1e29784 100644 --- a/Brewer/InstalledView.swift +++ b/Brewer/InstalledView.swift @@ -8,118 +8,101 @@ import SwiftUI struct InstalledView: View { - @State private var brew = Homebrew() - var body: some View { - VStack { - if brew.isLoading { - ProgressView("Loading...") - } else if let data = brew.data { - List { - if !data.casks.isEmpty { - Section { - ForEach(data.casks, id: \.fullToken) { cask in - NavigationLink { - NavigationStack { - VStack { - - Spacer() - } - .navigationTitle(cask.name.first ?? cask.fullToken) - } - } label: { - - HStack { - Text(cask.name.first ?? cask.fullToken) - Spacer() - if cask.outdated { - Button { - - } label: { - Label("Update", systemImage: "arrow.counterclockwise.circle.fill") - .symbolRenderingMode(.palette) - .foregroundStyle(.white, .orange) - } - } else { - Text(cask.version) - } - } - .contextMenu { - Button(role: .destructive) { - // brew.uninstall(app.0) - // brew.getInstalled() - } label: { - Label("Uninstall", systemImage: "trash") - } - } - } - } - } header: { - HStack { - Text("Casks") - Spacer() - Text("\(data.casks.count) installed") - } - } - } - - if !data.formulae.isEmpty { - Section { - ForEach(data.formulae, id: \.fullName) { formulae in - HStack { - Text(formulae.fullName) - Spacer() - if formulae.outdated { - Button { - - } label: { - Label("Update", systemImage: "arrow.counterclockwise.circle.fill") - .symbolRenderingMode(.palette) - .foregroundStyle(.white, .orange) - } - } else { - Text(formulae.versions.stable) - } - } - .contextMenu { - Button(role: .destructive) { - // brew.uninstall(app.0) - // brew.getInstalled() - } label: { - Label("Uninstall", systemImage: "trash") - } - } - } - } header: { - HStack { - Text("Formulae") - Spacer() - Text("\(data.formulae.count) installed") - } - } - } - } - .listStyle(.inset(alternatesRowBackgrounds: true)) - if data.casks.count + data.formulae.count > 0 { - Button { - - } label: { - Label("Update all", systemImage: "arrow.counterclockwise.circle.fill") - // .symbolRenderingMode(.palette) - // .foregroundStyle(.white, .orange) - } - .buttonStyle(.borderedProminent) - .tint(.orange) - } - } else if let error = brew.errorMessage { - Text(error) - .foregroundStyle(.red) - } - }.task { - brew.getInstalled() - } - } + @State private var brew = Homebrew() + var body: some View { + VStack { + if brew.isLoading { + ProgressView("Loading...") + } else if let data = brew.data { + List { + if !data.casks.isEmpty { + Section { + ForEach(data.casks, id: \.fullToken) { cask in + NavigationLink { + CaskDetailView(cask: cask, brewListing: brew) + } label: { + HStack { + Text(cask.name.first ?? cask.fullToken) + Spacer() + if cask.outdated { + Button { + + } label: { + Label("Update", systemImage: "arrow.counterclockwise.circle.fill") + .symbolRenderingMode(.palette) + .foregroundStyle(.white, .orange) + } + } else { + Text(cask.version) + } + } + .contextMenu { + UninstallButton(name: cask.fullToken, brewListing: brew) + } + } + } + } header: { + HStack { + Text("Casks") + Spacer() + Text("\(data.casks.count) installed") + } + } + } + + if !data.formulae.isEmpty { + Section { + ForEach(data.formulae, id: \.fullName) { formulae in + HStack { + Text(formulae.fullName) + Spacer() + if formulae.outdated { + Button { + + } label: { + Label("Update", systemImage: "arrow.counterclockwise.circle.fill") + .symbolRenderingMode(.palette) + .foregroundStyle(.white, .orange) + } + } else { + Text(formulae.versions.stable) + } + } + .contextMenu { + UninstallButton(name: formulae.fullName, brewListing: brew) + } + } + } header: { + HStack { + Text("Formulae") + Spacer() + Text("\(data.formulae.count) installed") + } + } + } + } + .listStyle(.inset(alternatesRowBackgrounds: true)) + let areOutdated = data.casks.map(\.outdated).count + data.formulae.map(\.outdated).count + let _ = UserDefaults.standard.set(!(areOutdated > 0), forKey: "isUpToDate") + if areOutdated > 0 { + Button { + + } label: { + Label("Update all", systemImage: "arrow.counterclockwise.circle.fill") + } + .buttonStyle(.borderedProminent) + .tint(.orange) + } + } else if let error = brew.errorMessage { + Text(error) + .foregroundStyle(.red) + } + }.task { + brew.getInstalled() + } + } } #Preview { - InstalledView() + InstalledView() } diff --git a/Brewer/Model/Homebrew.swift b/Brewer/Model/Homebrew.swift index ba19ab8..d2f539c 100644 --- a/Brewer/Model/Homebrew.swift +++ b/Brewer/Model/Homebrew.swift @@ -8,185 +8,207 @@ import Foundation enum ShellError: Error { - case emptyOutput + case emptyOutput } @discardableResult func shell(_ command: String) throws -> String { - let task = Process() - task.executableURL = URL(filePath: "/bin/bash") - task.arguments = ["-c", command] - - let pipe = Pipe() - task.standardError = pipe - task.standardOutput = pipe - - - try task.run() - if let data = try pipe.fileHandleForReading.readToEnd(), let output = String(data: data, encoding: .utf8) { - return output - } - - throw ShellError.emptyOutput + let task = Process() + task.executableURL = URL(filePath: "/bin/bash") + task.arguments = ["-c", command] + + let pipe = Pipe() + task.standardError = pipe + task.standardOutput = pipe + + try task.run() + if let data = try pipe.fileHandleForReading.readToEnd(), + let output = String(data: data, encoding: .utf8) + { + return output + } + + throw ShellError.emptyOutput } struct Cask: Codable { - let token: String - let fullToken: String - let tap: String - let name: [String] - let desc: String - let homepage: String - let url: String - let version: String - let installed: String? - let outdated: Bool - - enum CodingKeys: String, CodingKey { - case token, tap, name, desc, homepage, url, version, installed, outdated - case fullToken = "full_token" - } + let token: String + let fullToken: String + let tap: String + let name: [String] + let desc: String + let homepage: String + let url: String + let version: String + let installed: String? + let outdated: Bool + + enum CodingKeys: String, CodingKey { + case token, tap, name, desc, homepage, url, version, installed, outdated + case fullToken = "full_token" + } } struct Formulae: Codable { - - struct Version: Codable { - let stable: String - let head: String? - let bottle: Bool - - enum CodingKeys: String, CodingKey { - case stable - case head, bottle - } - } - - let name: String - let fullName: String - let tap: String - let oldNames: [String] - let aliases: [String] - let versionedFormulae: [String] - let desc: String - let license: String? - let homepage: String - let versions: Version - let outdated: Bool - - enum CodingKeys: String, CodingKey { - case name - case tap - case aliases - case desc - case license - case homepage - case versions - case outdated - case fullName = "full_name" - case oldNames = "oldnames" - case versionedFormulae = "versioned_formulae" - } + + struct Version: Codable { + let stable: String + let head: String? + let bottle: Bool + + enum CodingKeys: String, CodingKey { + case stable + case head, bottle + } + } + + let name: String + let fullName: String + let tap: String + let oldNames: [String] + let aliases: [String] + let versionedFormulae: [String] + let desc: String + let license: String? + let homepage: String + let versions: Version + let outdated: Bool + + enum CodingKeys: String, CodingKey { + case name + case tap + case aliases + case desc + case license + case homepage + case versions + case outdated + case fullName = "full_name" + case oldNames = "oldnames" + case versionedFormulae = "versioned_formulae" + } } struct InfoResponse: Codable { - let formulae: [Formulae] - let casks: [Cask] - - enum CodingKeys: String, CodingKey { - case casks - case formulae - } + let formulae: [Formulae] + let casks: [Cask] + + enum CodingKeys: String, CodingKey { + case casks + case formulae + } } @Observable class Homebrew { - var data: InfoResponse? - var isLoading = false - var errorMessage: String? - - func getInfo(on query: String) { - self.isLoading = true - self.errorMessage = nil - self.data = nil - Task { [weak self] in - do { - let res = try shell("/opt/homebrew/bin/brew info --json=v2 \(query)") - if let data = res.data(using: .utf8) { - let output = try JSONDecoder().decode(InfoResponse.self, from: data) - self?.data = output - } - } catch { - self?.errorMessage = error.localizedDescription - } - self?.isLoading = false - } - } - - func getInstalled() { - self.isLoading = true - self.data = nil - self.errorMessage = nil - Task { [weak self] in - do { - let res = try shell("/opt/homebrew/bin/brew info --json=v2 --installed") - print(res) - if let data = res.data(using: .utf8) { - let output = try JSONDecoder().decode(InfoResponse.self, from: data) - self?.data = output - } - } catch { - self?.errorMessage = error.localizedDescription - } - self?.isLoading = false - } - - } + var data: InfoResponse? + var isLoading = false + var errorMessage: String? -// func install(_ fullToken: String) where T == Bool { -// self.isLoading = true -// self.data = false -// Task { [weak self] in -// do { -// try shell("/opt/homebrew/bin/brew install --cask \(fullToken)") -// } catch { -// self?.errorMessage = error.localizedDescription -// } -// self?.isLoading = false -// do { -// var isInstalled = try shell("/opt/homebrew/bin/brew list \(fullToken)>/dev/null 2>&1 && echo true || false") -// let _ = isInstalled.popLast() -// let installed = isInstalled == "true" -// DispatchQueue.main.async { [weak self] in -// self?.data = installed -// } -// } catch { -// self?.errorMessage = error.localizedDescription -// } -// -// } -// } - -// func uninstall(_ fullToken: String) { -// self.isLoading = true -// self.data = false -// Task { [weak self] in -// do { -// try shell("/opt/homebrew/bin/brew uninstall \(fullToken)") -// } catch { -// self?.errorMessage = error.localizedDescription -// } -// self?.isLoading = false -// do { -// var isInstalled = try shell("/opt/homebrew/bin/brew list \(fullToken)>/dev/null 2>&1 && echo true || false") -// let _ = isInstalled.popLast() -// let installed = isInstalled != "true" -// DispatchQueue.main.async { [weak self] in -// self?.data = installed -// } -// } catch { -// self?.errorMessage = error.localizedDescription -// } -// } -// } + func getInfo(on query: String) { + self.isLoading = true + self.errorMessage = nil + self.data = nil + Task { [weak self] in + do { + let res = try shell("/opt/homebrew/bin/brew info --json=v2 \(query)") + if let data = res.data(using: .utf8) { + let output = try JSONDecoder().decode(InfoResponse.self, from: data) + self?.data = output + } + } catch { + self?.errorMessage = error.localizedDescription + } + self?.isLoading = false + } + } + + func getInstalled() { + self.isLoading = true + self.data = nil + self.errorMessage = nil + Task { [weak self] in + do { + let res = try shell("/opt/homebrew/bin/brew info --json=v2 --installed") + if let data = res.data(using: .utf8) { + let output = try JSONDecoder().decode(InfoResponse.self, from: data) + self?.data = output + } + } catch { + self?.errorMessage = error.localizedDescription + } + self?.isLoading = false + } + + } + + func isDownloaded(_ name: String) async -> Bool { + self.isLoading = true + self.errorMessage = nil + self.data = nil + let task = Task { [weak self] in + do { + let res = try shell("/opt/homebrew/bin/brew list -1 \(name) >/dev/null 2>&1; echo $?") + .trimmingCharacters(in: .whitespacesAndNewlines) + self?.isLoading = false + return res == "0" + } catch { + self?.errorMessage = error.localizedDescription + } + + return false + } + + switch await task.result { + case .success(let success): + return success + case .failure(let fail): + return false + } + } + + func install(_ fullToken: String, isCask: Bool = false) async -> Bool { + self.isLoading = true + self.data = nil + let task = Task { [weak self] in + do { + try shell("/opt/homebrew/bin/brew install \(isCask ? "--cask" : "") \(fullToken)") + } catch { + self?.errorMessage = error.localizedDescription + } + self?.isLoading = false + return await self?.isDownloaded(fullToken) ?? false + } + + return switch await task.result { + case .success(let success): + success + case .failure(let fail): + false + } + } + + func uninstall(_ fullToken: String) async -> Bool { + self.isLoading = true + self.data = nil + let task = Task { [weak self] in + do { + let res = + try shell("/opt/homebrew/bin/brew uninstall \(fullToken); echo $?") + .trimmingCharacters(in: .whitespacesAndNewlines) + } catch { + self?.errorMessage = error.localizedDescription + } + self?.isLoading = false + return await !(self?.isDownloaded(fullToken) ?? true) + } + + return switch await task.result { + case .success(let success): + success + case .failure(let failure): + false + } + + } } - diff --git a/Brewer/SearchView.swift b/Brewer/SearchView.swift index aa0678b..93d9e3b 100644 --- a/Brewer/SearchView.swift +++ b/Brewer/SearchView.swift @@ -8,68 +8,58 @@ import SwiftUI struct SearchView: View { - @State private var query = "" - @State private var brew = Homebrew() - var body: some View { - VStack { - TextField("Search", text: $query) - .padding() - .padding(.bottom, 0) - .onSubmit { - brew.getInfo(on: query) - } - Spacer() - if brew.isLoading { - ProgressView("Loading...") - } else if let data = brew.data { - List { - Section("Casks") { - ForEach(data.casks, id: \.fullToken) { cask in - HStack { - Text(cask.fullToken) - Spacer() - if cask.installed != nil { - Image(systemName: "checkmark.circle.fill") - .symbolRenderingMode(.palette) - .foregroundStyle(.white, .green) - } else { - if !brew.isLoading && brew.errorMessage == nil { - Button("Get") { -// brew.install(cask.fullToken) -// #warning("Doesn't work, doesn't know why; concurency issue") -// if let isInstalled = brew.data, isInstalled { -// brew.getInfo(on: cask.fullToken) -// } - } - .buttonStyle(.borderedProminent) - .buttonBorderShape(.capsule) - .tint(.accentColor) - } else if brew.isLoading { - ProgressView() - .frame(width: 10, height: 10) - } else if let error = brew.errorMessage { - Image(systemName: "x.circle.fill") - .symbolRenderingMode(.palette) - .foregroundStyle(.white, .red) - } - } - } - } - } - Section("Formulaes") { -// ForEach(data.formulae, id: \.self) {formulae in -// -// } - } - } - } else if let errorMessage = brew.errorMessage { - Text(errorMessage) - .foregroundStyle(.red) - } - } + @State private var query = "" + @State private var brew = Homebrew() + var body: some View { + VStack { + TextField("Search", text: $query) + .padding() + .padding(.bottom, 0) + .onSubmit { + brew.getInfo(on: query) + } + Spacer() + if brew.isLoading { + ProgressView("Loading...") + } else if let data = brew.data { + List { + if !data.casks.isEmpty { + Section("Casks") { + ForEach(data.casks, id: \.fullToken) { cask in + HStack { + Text(cask.fullToken) + Spacer() + if cask.installed != nil { + Image(systemName: "checkmark.circle.fill") + .symbolRenderingMode(.palette) + .foregroundStyle(.white, .green) + } else { + DownloadButton(name: cask.fullToken, isCask: true) + } + } + } + } + } + if !data.formulae.isEmpty { + + Section("Formulaes") { + ForEach(data.formulae, id: \.fullName) { formulae in + HStack { + Text(formulae.fullName) + } + } + } + } + } + } else if let errorMessage = brew.errorMessage { + Text(errorMessage) + .foregroundStyle(.red) + } } + } + } #Preview { - SearchView() + SearchView() }