Skip to content

Commit

Permalink
Rework brew management UI
Browse files Browse the repository at this point in the history
  • Loading branch information
milanvarady committed Jan 1, 2025
1 parent 0ac1bc7 commit c898fee
Show file tree
Hide file tree
Showing 12 changed files with 364 additions and 273 deletions.
18 changes: 17 additions & 1 deletion Applite.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@
419256A42D25CFE600D9EF10 /* AppMigrationView+ExportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 419256A32D25CFE600D9EF10 /* AppMigrationView+ExportView.swift */; };
419256A62D25D00200D9EF10 /* AppMigrationView+ImportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 419256A52D25D00200D9EF10 /* AppMigrationView+ImportView.swift */; };
419256A82D25D10F00D9EF10 /* ExportFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 419256A72D25D10F00D9EF10 /* ExportFile.swift */; };
419256AB2D25E19B00D9EF10 /* BrewManagementView+ActionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 419256AA2D25E19B00D9EF10 /* BrewManagementView+ActionsView.swift */; };
419256AD2D25E1F100D9EF10 /* BrewManagementView+InfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 419256AC2D25E1F100D9EF10 /* BrewManagementView+InfoView.swift */; };
419506A42964A27F00FE5802 /* SetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 419506A32964A27F00FE5802 /* SetupView.swift */; };
419506A62964A5EF00FE5802 /* BrewPathSelectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 419506A52964A5EF00FE5802 /* BrewPathSelectorView.swift */; };
4196C8F528F9CB2600EADDDA /* DiscoverView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4196C8F428F9CB2600EADDDA /* DiscoverView.swift */; };
Expand Down Expand Up @@ -216,6 +218,8 @@
419256A32D25CFE600D9EF10 /* AppMigrationView+ExportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppMigrationView+ExportView.swift"; sourceTree = "<group>"; };
419256A52D25D00200D9EF10 /* AppMigrationView+ImportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppMigrationView+ImportView.swift"; sourceTree = "<group>"; };
419256A72D25D10F00D9EF10 /* ExportFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExportFile.swift; sourceTree = "<group>"; };
419256AA2D25E19B00D9EF10 /* BrewManagementView+ActionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BrewManagementView+ActionsView.swift"; sourceTree = "<group>"; };
419256AC2D25E1F100D9EF10 /* BrewManagementView+InfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BrewManagementView+InfoView.swift"; sourceTree = "<group>"; };
419506A32964A27F00FE5802 /* SetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupView.swift; sourceTree = "<group>"; };
419506A52964A5EF00FE5802 /* BrewPathSelectorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrewPathSelectorView.swift; sourceTree = "<group>"; };
419506A729696A5300FE5802 /* Applite-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "Applite-Info.plist"; sourceTree = SOURCE_ROOT; };
Expand Down Expand Up @@ -612,6 +616,16 @@
path = "App Migration";
sourceTree = "<group>";
};
419256A92D25E17900D9EF10 /* Brew Management */ = {
isa = PBXGroup;
children = (
418989AC2A33A5C4004AC23B /* BrewManagementView.swift */,
419256AC2D25E1F100D9EF10 /* BrewManagementView+InfoView.swift */,
419256AA2D25E19B00D9EF10 /* BrewManagementView+ActionsView.swift */,
);
path = "Brew Management";
sourceTree = "<group>";
};
4196C8F628F9CB4100EADDDA /* App Views */ = {
isa = PBXGroup;
children = (
Expand All @@ -634,7 +648,7 @@
41B731382A879353008BF6B9 /* ActiveTasksView.swift */,
419256A22D25CFBF00D9EF10 /* App Migration */,
41857B742912D94A004A1894 /* CategoryView.swift */,
418989AC2A33A5C4004AC23B /* BrewManagementView.swift */,
419256A92D25E17900D9EF10 /* Brew Management */,
);
path = "Detail Views";
sourceTree = "<group>";
Expand Down Expand Up @@ -768,6 +782,7 @@
419256182D1DEA4400D9EF10 /* AppView+ActionsView.swift in Sources */,
413F77A52972B2E70053349A /* DependencyManager.swift in Sources */,
418989B42A35D67C004AC23B /* isCommandLineToolsInstalled.swift in Sources */,
419256AB2D25E19B00D9EF10 /* BrewManagementView+ActionsView.swift in Sources */,
419256352D1DF30700D9EF10 /* SettingsView+UpdateSettings.swift in Sources */,
4192563C2D1DF3C900D9EF10 /* ContentView+SidebarViews.swift in Sources */,
4192560F2D1CC09500D9EF10 /* DependencyError.swift in Sources */,
Expand Down Expand Up @@ -833,6 +848,7 @@
419256912D23F93E00D9EF10 /* Card.swift in Sources */,
419256252D1DF17F00D9EF10 /* SetupView+Welcome.swift in Sources */,
419256082D1C734600D9EF10 /* Shell.swift in Sources */,
419256AD2D25E1F100D9EF10 /* BrewManagementView+InfoView.swift in Sources */,
4192563E2D1DF3E900D9EF10 /* ContentView+DetailView.swift in Sources */,
419256162D1DEA0A00D9EF10 /* AppView+IconAndDescriptionView.swift in Sources */,
419256942D24255000D9EF10 /* CaskLoadError.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ extension CaskManager {
runTask(for: cask) {
cask.progressState = .busy(withTask: "Uninstalling")

var arguments: [String] = [cask.info.id]
var arguments: [String] = ["uninstall", "--cask", cask.info.id]

// Add -- zap argument
if zap {
Expand All @@ -95,7 +95,7 @@ extension CaskManager {
var output: String = ""

do {
output = try await Shell.runBrewCommand("uninstall", arguments: arguments)
output = try await Shell.runBrewCommand(arguments)
} catch {
await self.showFailure(
for: cask,
Expand Down Expand Up @@ -127,7 +127,7 @@ extension CaskManager {
var output: String = ""

do {
output = try await Shell.runBrewCommand("upgrade", arguments: [cask.info.id])
output = try await Shell.runBrewCommand(["upgrade", "--cask", cask.info.id])
} catch {
await self.showFailure(
for: cask,
Expand Down Expand Up @@ -158,7 +158,7 @@ extension CaskManager {
var output: String = ""

do {
output = try await Shell.runBrewCommand("reinstall", arguments: [cask.info.id])
output = try await Shell.runBrewCommand(["reinstall", "--cask", cask.info.id])
} catch {
await self.showFailure(
for: cask,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ extension CaskManager {
/// - Returns: A list of Cask ID's
@Sendable
func getInstalledCasks() async throws -> [String] {
let output = try await Shell.runAsync("\(BrewPaths.currentBrewExecutable) list --cask")
let output = try await Shell.runBrewCommand(["list", "--cask"])

if output.isEmpty {
await Self.logger.notice("No installed casks were found")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ import Foundation

extension CaskManager {
func refreshOutdated(greedy: Bool = false) async throws -> Void {
var arguments: [String] = ["-q"]
var arguments: [String] = ["outdated", "--cask", "-q"]

if greedy {
arguments.append("-g")
}

let output = try await Shell.runBrewCommand("outdated", arguments: arguments)
let output = try await Shell.runBrewCommand(arguments)

let outdatedCaskIDs = output
.trimmingCharacters(in: .whitespacesAndNewlines)
Expand Down
2 changes: 1 addition & 1 deletion Applite/Utilities/App Migration/ExportCasks.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ enum CaskImportError: Error {

enum AppMigration {
static func export() async throws -> ExportFile {
let output = try await Shell.runBrewCommand("list")
let output = try await Shell.runBrewCommand(["list", "--cask"])

let exportedCasks = output.trimmingCharacters(in: .whitespacesAndNewlines)

Expand Down
4 changes: 2 additions & 2 deletions Applite/Utilities/Shell/Shell.swift
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,8 @@ enum Shell {
///
/// Using the `pty` option can leave unwanted characters in the output, use only when necessary
@discardableResult
static func runBrewCommand(_ brewCommand: String, arguments: [String] = [], pty: Bool = false) async throws -> String {
let command = "\(BrewPaths.currentBrewExecutable) \(brewCommand) --cask \(arguments.joined(separator: " "))"
static func runBrewCommand(_ arguments: [String], pty: Bool = false) async throws -> String {
let command = "\(BrewPaths.currentBrewExecutable) \(arguments.joined(separator: " "))"
return try await runAsync(command)
}

Expand Down
2 changes: 1 addition & 1 deletion Applite/Views/Content View/ContentView+LoadCasks.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ extension ContentView {
loadAlert.show(title: "Couldn't load app catalog", message: DependencyManager.brokenPathOrIstallMessage)
brokenInstall = true

let output = (try? await Shell.runAsync("\(BrewPaths.currentBrewExecutable) --version")) ?? "n/a"
let output = (try? await Shell.runBrewCommand(["--version"])) ?? "n/a"

logger.error(
"""
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
//
// BrewManagementView+ActionsView.swift
// Applite
//
// Created by Milán Várady on 2025.01.01.
//

import SwiftUI

extension BrewManagementView {
struct ActionsView: View {
@Binding var modifyingBrew: Bool
let cardWidth: CGFloat
let cardPadding: CGFloat
let cardHeight: CGFloat = 210

@State var updateDone = false
@State var reinstallDone = false

@State var isAppBrewInstalled = false

@State var isPresentingReinstallConfirm = false

@State var updateFailed = false
@State var reinstallFailed = false

private struct Remark: Identifiable {
let title: LocalizedStringKey
let color: Color
let remark: LocalizedStringKey

var id = UUID()
}

var body: some View {
VStack(alignment: .leading) {
Text("Actions")
.font(.appliteSmallTitle)

HStack {
ActionCard(
cardWidth: cardWidth,
cardHeight: cardHeight,
paddig: cardPadding,
actionSuccessful: $updateDone,
remarks: [
.init(title: "Warning", color: .orange, remark: "All other app functions will be disabled during the update!")
]
) {
updateButton
}

ActionCard(
cardWidth: cardWidth,
cardHeight: cardHeight,
paddig: cardPadding,
actionSuccessful: $reinstallDone,
remarks: [
.init(title: "Note", color: .blue, remark: "This will (re)install \(Bundle.main.appName)'s Homebrew installation at: ~/Library/Application Support/\(Bundle.main.appName)/homebrew"),
.init(title: "Warning", color: .orange, remark: "After reinstalling, all currently installed apps will be unlinked from \(Bundle.main.appName). They won't be deleted, but you won't be able to update or uninstall them via \(Bundle.main.appName).")
]
) {
reinstallButton
}
}
.padding(.bottom, 10)

// Progress indicator
if modifyingBrew {
HStack {
Text("In progress...")
.bold()

SmallProgressView()
}
}
}
.task {
// Check if brew is installed in application support
isAppBrewInstalled = await isBrewPathValid(path: BrewPaths.getBrewExectuablePath(for: .appPath))
}
}

private struct ActionCard<ActionButton: View>: View {
let cardWidth: CGFloat
let cardHeight: CGFloat
let paddig: CGFloat
@Binding var actionSuccessful: Bool
let remarks: [Remark]
@ViewBuilder let actionButton: ActionButton

var body: some View {
Card(cardWidth: cardWidth, cardHeight: cardHeight, paddig: paddig) {
VStack(alignment: .leading) {
HStack {
actionButton

// Success checkmark
if actionSuccessful {
Image(systemName: "checkmark.circle")
.imageScale(.large)
.foregroundStyle(.green)
}
}
.padding(.bottom, 12)

VStack(alignment: .leading, spacing: 5) {
ForEach(remarks) { remark in
Text(remark.title)
.foregroundColor(remark.color)
.fontWeight(.bold)
+
Text(": ")
.foregroundColor(remark.color)
.fontWeight(.bold)
+
Text(remark.remark)
}
}

Spacer()
}
}
}
}

@MainActor
private var updateButton: some View {
Button {
withAnimation {
modifyingBrew = true
}

Task {
logger.info("Updating brew started")

do {
try await Shell.runBrewCommand(["update"])
} catch {
logger.error("Brew update failed. Error: \(error.localizedDescription)")
updateFailed = true
}

logger.info("Brew update successful")

updateDone = true

withAnimation {
modifyingBrew = false
}

}
} label: {
Label("Update Homebrew", systemImage: "arrow.uturn.down.circle")
}
.controlSize(.large)
.disabled(modifyingBrew)
.padding(.trailing, 3)
.alert("Update failed", isPresented: $updateFailed, actions: {})
}

@MainActor
private var reinstallButton: some View {
Button(role: .destructive) {
isPresentingReinstallConfirm = true
} label: {
Label(isAppBrewInstalled ? "Reinstall Homebrew" : "Install Separate Brew", systemImage: "wrench.and.screwdriver")
}
.controlSize(.large)
.disabled(modifyingBrew)
.confirmationDialog("Are you sure you want to \(isAppBrewInstalled ? "re" : "")install Homebrew?", isPresented: $isPresentingReinstallConfirm) {
Button("Reinstall", role: .destructive) {
withAnimation {
modifyingBrew = true
}

Task {
do {
try await DependencyManager.installHomebrew()
} catch {
reinstallFailed = true
}

if !reinstallFailed {
reinstallDone = true
}

withAnimation {
modifyingBrew = false
}
}
}

Button("Cancel", role: .cancel) { }
} message: {
if isAppBrewInstalled {
Text("All currently installed apps will be unlinked from \(Bundle.main.appName).")
} else {
Text("A new Homebrew installation will be installed into ~/Library/Application Support/\(Bundle.main.appName)")
}
}
.alert("Reinstall failed", isPresented: $reinstallFailed, actions: {
Button("OK", role: .cancel) { }
})
}
}
}
Loading

0 comments on commit c898fee

Please sign in to comment.