Better reactivity and functionality

This commit is contained in:
cdricms
2024-07-09 20:29:20 +02:00
parent bd9c351b7c
commit c675a98309
9 changed files with 541 additions and 361 deletions

View File

@@ -9,6 +9,9 @@
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
9E6C73072C3D5E570056ADDC /* SearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E6C73062C3D5E570056ADDC /* SearchView.swift */; }; 9E6C73072C3D5E570056ADDC /* SearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E6C73062C3D5E570056ADDC /* SearchView.swift */; };
9E6C73092C3D5E950056ADDC /* InstalledView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E6C73082C3D5E950056ADDC /* InstalledView.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 */; }; 9E8CE5362C3C545600A39146 /* BrewerApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E8CE5352C3C545600A39146 /* BrewerApp.swift */; };
9E8CE5382C3C545600A39146 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E8CE5372C3C545600A39146 /* ContentView.swift */; }; 9E8CE5382C3C545600A39146 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E8CE5372C3C545600A39146 /* ContentView.swift */; };
9E8CE53A2C3C545700A39146 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9E8CE5392C3C545700A39146 /* Assets.xcassets */; }; 9E8CE53A2C3C545700A39146 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9E8CE5392C3C545700A39146 /* Assets.xcassets */; };
@@ -39,6 +42,9 @@
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
9E6C73062C3D5E570056ADDC /* SearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchView.swift; sourceTree = "<group>"; }; 9E6C73062C3D5E570056ADDC /* SearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchView.swift; sourceTree = "<group>"; };
9E6C73082C3D5E950056ADDC /* InstalledView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstalledView.swift; sourceTree = "<group>"; }; 9E6C73082C3D5E950056ADDC /* InstalledView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstalledView.swift; sourceTree = "<group>"; };
9E6C730B2C3D796D0056ADDC /* DownloadButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadButton.swift; sourceTree = "<group>"; };
9E6C730D2C3DB16F0056ADDC /* UninstallButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UninstallButton.swift; sourceTree = "<group>"; };
9E6C73102C3DB5940056ADDC /* CaskDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CaskDetailView.swift; sourceTree = "<group>"; };
9E8CE5322C3C545600A39146 /* Brewer.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Brewer.app; sourceTree = BUILT_PRODUCTS_DIR; }; 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 = "<group>"; }; 9E8CE5352C3C545600A39146 /* BrewerApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrewerApp.swift; sourceTree = "<group>"; };
9E8CE5372C3C545600A39146 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; }; 9E8CE5372C3C545600A39146 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
@@ -78,6 +84,23 @@
/* End PBXFrameworksBuildPhase section */ /* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */ /* Begin PBXGroup section */
9E6C730A2C3D795D0056ADDC /* Components */ = {
isa = PBXGroup;
children = (
9E6C730B2C3D796D0056ADDC /* DownloadButton.swift */,
9E6C730D2C3DB16F0056ADDC /* UninstallButton.swift */,
);
path = Components;
sourceTree = "<group>";
};
9E6C730F2C3DB5850056ADDC /* Details */ = {
isa = PBXGroup;
children = (
9E6C73102C3DB5940056ADDC /* CaskDetailView.swift */,
);
name = Details;
sourceTree = "<group>";
};
9E8CE5292C3C545600A39146 = { 9E8CE5292C3C545600A39146 = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@@ -101,9 +124,11 @@
9E8CE5342C3C545600A39146 /* Brewer */ = { 9E8CE5342C3C545600A39146 /* Brewer */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
9E6C730A2C3D795D0056ADDC /* Components */,
9E8CE5602C3C5A5000A39146 /* Model */, 9E8CE5602C3C5A5000A39146 /* Model */,
9E8CE5352C3C545600A39146 /* BrewerApp.swift */, 9E8CE5352C3C545600A39146 /* BrewerApp.swift */,
9E6C73062C3D5E570056ADDC /* SearchView.swift */, 9E6C73062C3D5E570056ADDC /* SearchView.swift */,
9E6C730F2C3DB5850056ADDC /* Details */,
9E6C73082C3D5E950056ADDC /* InstalledView.swift */, 9E6C73082C3D5E950056ADDC /* InstalledView.swift */,
9E8CE5372C3C545600A39146 /* ContentView.swift */, 9E8CE5372C3C545600A39146 /* ContentView.swift */,
9E8CE5392C3C545700A39146 /* Assets.xcassets */, 9E8CE5392C3C545700A39146 /* Assets.xcassets */,
@@ -277,10 +302,13 @@
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
9E6C73072C3D5E570056ADDC /* SearchView.swift in Sources */, 9E6C73072C3D5E570056ADDC /* SearchView.swift in Sources */,
9E6C730C2C3D796D0056ADDC /* DownloadButton.swift in Sources */,
9E8CE5382C3C545600A39146 /* ContentView.swift in Sources */, 9E8CE5382C3C545600A39146 /* ContentView.swift in Sources */,
9E6C730E2C3DB16F0056ADDC /* UninstallButton.swift in Sources */,
9E8CE5622C3C5A6A00A39146 /* Homebrew.swift in Sources */, 9E8CE5622C3C5A6A00A39146 /* Homebrew.swift in Sources */,
9E6C73092C3D5E950056ADDC /* InstalledView.swift in Sources */, 9E6C73092C3D5E950056ADDC /* InstalledView.swift in Sources */,
9E8CE5362C3C545600A39146 /* BrewerApp.swift in Sources */, 9E8CE5362C3C545600A39146 /* BrewerApp.swift in Sources */,
9E6C73112C3DB5940056ADDC /* CaskDetailView.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };

View File

@@ -9,12 +9,34 @@ import SwiftUI
@main @main
struct BrewerApp: App { struct BrewerApp: App {
var body: some Scene { @AppStorage("isUpToDate") var isUpToDate: Bool = true
WindowGroup { init() {
ContentView() do {
} let res = try shell("/opt/homebrew/bin/brew outdated --greedy-latest -g | wc -l")
MenuBarExtra("Brewer", systemImage: "sparkle.magnifyingglass") { if let number = Int(res.trimmingCharacters(in: .whitespacesAndNewlines)), number > 0 {
ContentView().padding() isUpToDate = false
}.menuBarExtraStyle(.window) }
} 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)
}
} }

View File

@@ -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)
}
}
}
}
}
}

View File

@@ -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")
}

View File

@@ -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)
}
}
}
}

View File

@@ -8,30 +8,29 @@
import SwiftUI import SwiftUI
enum SegementedSelection: String, CaseIterable { enum SegementedSelection: String, CaseIterable {
case search = "Search" case search = "Search"
case installed = "Installed" case installed = "Installed"
} }
struct ContentView: View { struct ContentView: View {
@State private var segmentedSelection: SegementedSelection = .search @State private var segmentedSelection: SegementedSelection = .search
var body: some View { var body: some View {
NavigationStack { NavigationStack {
Picker("", selection: $segmentedSelection) {
Picker("", selection: $segmentedSelection) { ForEach(SegementedSelection.allCases, id: \.self) { sel in
ForEach(SegementedSelection.allCases, id: \.self) { sel in Text(sel.rawValue).tag(sel)
Text(sel.rawValue).tag(sel) }
} }.pickerStyle(.segmented)
}.pickerStyle(.segmented) switch segmentedSelection {
switch segmentedSelection { case .search:
case .search: SearchView()
SearchView() case .installed:
case .installed: InstalledView()
InstalledView() }
} }
} }
}
} }
#Preview { #Preview {
ContentView() ContentView()
} }

View File

@@ -8,118 +8,101 @@
import SwiftUI import SwiftUI
struct InstalledView: View { struct InstalledView: View {
@State private var brew = Homebrew() @State private var brew = Homebrew()
var body: some View { var body: some View {
VStack { VStack {
if brew.isLoading { if brew.isLoading {
ProgressView("Loading...") ProgressView("Loading...")
} else if let data = brew.data { } else if let data = brew.data {
List { List {
if !data.casks.isEmpty { if !data.casks.isEmpty {
Section { Section {
ForEach(data.casks, id: \.fullToken) { cask in ForEach(data.casks, id: \.fullToken) { cask in
NavigationLink { NavigationLink {
NavigationStack { CaskDetailView(cask: cask, brewListing: brew)
VStack { } label: {
HStack {
Spacer() Text(cask.name.first ?? cask.fullToken)
} Spacer()
.navigationTitle(cask.name.first ?? cask.fullToken) if cask.outdated {
} Button {
} label: {
} label: {
HStack { Label("Update", systemImage: "arrow.counterclockwise.circle.fill")
Text(cask.name.first ?? cask.fullToken) .symbolRenderingMode(.palette)
Spacer() .foregroundStyle(.white, .orange)
if cask.outdated { }
Button { } else {
Text(cask.version)
} label: { }
Label("Update", systemImage: "arrow.counterclockwise.circle.fill") }
.symbolRenderingMode(.palette) .contextMenu {
.foregroundStyle(.white, .orange) UninstallButton(name: cask.fullToken, brewListing: brew)
} }
} else { }
Text(cask.version) }
} } header: {
} HStack {
.contextMenu { Text("Casks")
Button(role: .destructive) { Spacer()
// brew.uninstall(app.0) Text("\(data.casks.count) installed")
// brew.getInstalled() }
} label: { }
Label("Uninstall", systemImage: "trash") }
}
} if !data.formulae.isEmpty {
} Section {
} ForEach(data.formulae, id: \.fullName) { formulae in
} header: { HStack {
HStack { Text(formulae.fullName)
Text("Casks") Spacer()
Spacer() if formulae.outdated {
Text("\(data.casks.count) installed") Button {
}
} } label: {
} Label("Update", systemImage: "arrow.counterclockwise.circle.fill")
.symbolRenderingMode(.palette)
if !data.formulae.isEmpty { .foregroundStyle(.white, .orange)
Section { }
ForEach(data.formulae, id: \.fullName) { formulae in } else {
HStack { Text(formulae.versions.stable)
Text(formulae.fullName) }
Spacer() }
if formulae.outdated { .contextMenu {
Button { UninstallButton(name: formulae.fullName, brewListing: brew)
}
} label: { }
Label("Update", systemImage: "arrow.counterclockwise.circle.fill") } header: {
.symbolRenderingMode(.palette) HStack {
.foregroundStyle(.white, .orange) Text("Formulae")
} Spacer()
} else { Text("\(data.formulae.count) installed")
Text(formulae.versions.stable) }
} }
} }
.contextMenu { }
Button(role: .destructive) { .listStyle(.inset(alternatesRowBackgrounds: true))
// brew.uninstall(app.0) let areOutdated = data.casks.map(\.outdated).count + data.formulae.map(\.outdated).count
// brew.getInstalled() let _ = UserDefaults.standard.set(!(areOutdated > 0), forKey: "isUpToDate")
} label: { if areOutdated > 0 {
Label("Uninstall", systemImage: "trash") Button {
}
} } label: {
} Label("Update all", systemImage: "arrow.counterclockwise.circle.fill")
} header: { }
HStack { .buttonStyle(.borderedProminent)
Text("Formulae") .tint(.orange)
Spacer() }
Text("\(data.formulae.count) installed") } else if let error = brew.errorMessage {
} Text(error)
} .foregroundStyle(.red)
} }
} }.task {
.listStyle(.inset(alternatesRowBackgrounds: true)) brew.getInstalled()
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()
}
}
} }
#Preview { #Preview {
InstalledView() InstalledView()
} }

View File

@@ -8,185 +8,207 @@
import Foundation import Foundation
enum ShellError: Error { enum ShellError: Error {
case emptyOutput case emptyOutput
} }
@discardableResult @discardableResult
func shell(_ command: String) throws -> String { func shell(_ command: String) throws -> String {
let task = Process() let task = Process()
task.executableURL = URL(filePath: "/bin/bash") task.executableURL = URL(filePath: "/bin/bash")
task.arguments = ["-c", command] task.arguments = ["-c", command]
let pipe = Pipe() let pipe = Pipe()
task.standardError = pipe task.standardError = pipe
task.standardOutput = pipe task.standardOutput = pipe
try task.run()
try task.run() if let data = try pipe.fileHandleForReading.readToEnd(),
if let data = try pipe.fileHandleForReading.readToEnd(), let output = String(data: data, encoding: .utf8) { let output = String(data: data, encoding: .utf8)
return output {
} return output
}
throw ShellError.emptyOutput
throw ShellError.emptyOutput
} }
struct Cask: Codable { struct Cask: Codable {
let token: String let token: String
let fullToken: String let fullToken: String
let tap: String let tap: String
let name: [String] let name: [String]
let desc: String let desc: String
let homepage: String let homepage: String
let url: String let url: String
let version: String let version: String
let installed: String? let installed: String?
let outdated: Bool let outdated: Bool
enum CodingKeys: String, CodingKey { enum CodingKeys: String, CodingKey {
case token, tap, name, desc, homepage, url, version, installed, outdated case token, tap, name, desc, homepage, url, version, installed, outdated
case fullToken = "full_token" case fullToken = "full_token"
} }
} }
struct Formulae: Codable { struct Formulae: Codable {
struct Version: Codable { struct Version: Codable {
let stable: String let stable: String
let head: String? let head: String?
let bottle: Bool let bottle: Bool
enum CodingKeys: String, CodingKey { enum CodingKeys: String, CodingKey {
case stable case stable
case head, bottle case head, bottle
} }
} }
let name: String let name: String
let fullName: String let fullName: String
let tap: String let tap: String
let oldNames: [String] let oldNames: [String]
let aliases: [String] let aliases: [String]
let versionedFormulae: [String] let versionedFormulae: [String]
let desc: String let desc: String
let license: String? let license: String?
let homepage: String let homepage: String
let versions: Version let versions: Version
let outdated: Bool let outdated: Bool
enum CodingKeys: String, CodingKey { enum CodingKeys: String, CodingKey {
case name case name
case tap case tap
case aliases case aliases
case desc case desc
case license case license
case homepage case homepage
case versions case versions
case outdated case outdated
case fullName = "full_name" case fullName = "full_name"
case oldNames = "oldnames" case oldNames = "oldnames"
case versionedFormulae = "versioned_formulae" case versionedFormulae = "versioned_formulae"
} }
} }
struct InfoResponse: Codable { struct InfoResponse: Codable {
let formulae: [Formulae] let formulae: [Formulae]
let casks: [Cask] let casks: [Cask]
enum CodingKeys: String, CodingKey { enum CodingKeys: String, CodingKey {
case casks case casks
case formulae case formulae
} }
} }
@Observable @Observable
class Homebrew { class Homebrew {
var data: InfoResponse? var data: InfoResponse?
var isLoading = false var isLoading = false
var errorMessage: String? 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
}
}
// func install(_ fullToken: String) where T == Bool { func getInfo(on query: String) {
// self.isLoading = true self.isLoading = true
// self.data = false self.errorMessage = nil
// Task { [weak self] in self.data = nil
// do { Task { [weak self] in
// try shell("/opt/homebrew/bin/brew install --cask \(fullToken)") do {
// } catch { let res = try shell("/opt/homebrew/bin/brew info --json=v2 \(query)")
// self?.errorMessage = error.localizedDescription if let data = res.data(using: .utf8) {
// } let output = try JSONDecoder().decode(InfoResponse.self, from: data)
// self?.isLoading = false self?.data = output
// do { }
// var isInstalled = try shell("/opt/homebrew/bin/brew list \(fullToken)>/dev/null 2>&1 && echo true || false") } catch {
// let _ = isInstalled.popLast() self?.errorMessage = error.localizedDescription
// let installed = isInstalled == "true" }
// DispatchQueue.main.async { [weak self] in self?.isLoading = false
// self?.data = installed }
// } }
// } catch {
// self?.errorMessage = error.localizedDescription func getInstalled() {
// } self.isLoading = true
// self.data = nil
// } self.errorMessage = nil
// } Task { [weak self] in
do {
// func uninstall(_ fullToken: String) { let res = try shell("/opt/homebrew/bin/brew info --json=v2 --installed")
// self.isLoading = true if let data = res.data(using: .utf8) {
// self.data = false let output = try JSONDecoder().decode(InfoResponse.self, from: data)
// Task { [weak self] in self?.data = output
// do { }
// try shell("/opt/homebrew/bin/brew uninstall \(fullToken)") } catch {
// } catch { self?.errorMessage = error.localizedDescription
// self?.errorMessage = error.localizedDescription }
// } self?.isLoading = false
// 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" func isDownloaded(_ name: String) async -> Bool {
// DispatchQueue.main.async { [weak self] in self.isLoading = true
// self?.data = installed self.errorMessage = nil
// } self.data = nil
// } catch { let task = Task { [weak self] in
// self?.errorMessage = error.localizedDescription 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
}
}
} }

View File

@@ -8,68 +8,58 @@
import SwiftUI import SwiftUI
struct SearchView: View { struct SearchView: View {
@State private var query = "" @State private var query = ""
@State private var brew = Homebrew() @State private var brew = Homebrew()
var body: some View { var body: some View {
VStack { VStack {
TextField("Search", text: $query) TextField("Search", text: $query)
.padding() .padding()
.padding(.bottom, 0) .padding(.bottom, 0)
.onSubmit { .onSubmit {
brew.getInfo(on: query) brew.getInfo(on: query)
} }
Spacer() Spacer()
if brew.isLoading { if brew.isLoading {
ProgressView("Loading...") ProgressView("Loading...")
} else if let data = brew.data { } else if let data = brew.data {
List { List {
Section("Casks") { if !data.casks.isEmpty {
ForEach(data.casks, id: \.fullToken) { cask in Section("Casks") {
HStack { ForEach(data.casks, id: \.fullToken) { cask in
Text(cask.fullToken) HStack {
Spacer() Text(cask.fullToken)
if cask.installed != nil { Spacer()
Image(systemName: "checkmark.circle.fill") if cask.installed != nil {
.symbolRenderingMode(.palette) Image(systemName: "checkmark.circle.fill")
.foregroundStyle(.white, .green) .symbolRenderingMode(.palette)
} else { .foregroundStyle(.white, .green)
if !brew.isLoading && brew.errorMessage == nil { } else {
Button("Get") { DownloadButton(name: cask.fullToken, isCask: true)
// 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) }
// } }
} if !data.formulae.isEmpty {
.buttonStyle(.borderedProminent)
.buttonBorderShape(.capsule) Section("Formulaes") {
.tint(.accentColor) ForEach(data.formulae, id: \.fullName) { formulae in
} else if brew.isLoading { HStack {
ProgressView() Text(formulae.fullName)
.frame(width: 10, height: 10) }
} else if let error = brew.errorMessage { }
Image(systemName: "x.circle.fill") }
.symbolRenderingMode(.palette) }
.foregroundStyle(.white, .red) }
} } else if let errorMessage = brew.errorMessage {
} Text(errorMessage)
} .foregroundStyle(.red)
} }
}
Section("Formulaes") {
// ForEach(data.formulae, id: \.self) {formulae in
//
// }
}
}
} else if let errorMessage = brew.errorMessage {
Text(errorMessage)
.foregroundStyle(.red)
}
}
} }
}
} }
#Preview { #Preview {
SearchView() SearchView()
} }