Skip to content

Commit ffa4ab1

Browse files
committed
Check brew during onboarding and prompt to install missing deps
1 parent fe3ad18 commit ffa4ab1

1 file changed

Lines changed: 185 additions & 32 deletions

File tree

airsync-mac/Screens/OnboardingView/MirroringSetupView.swift

Lines changed: 185 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
//
77

88
import SwiftUI
9+
import AppKit
910

1011
struct MirroringSetupView: View {
1112
let onNext: () -> Void
@@ -15,6 +16,9 @@ struct MirroringSetupView: View {
1516
@State private var scrcpyAvailable = false
1617
@State private var mediaControlAvailable = false
1718
@State private var checking = true
19+
@State private var brewAvailable = false
20+
@State private var installingPackage: String? = nil
21+
@State private var installLog: String = ""
1822

1923
var body: some View {
2024
VStack(spacing: 20) {
@@ -62,42 +66,106 @@ struct MirroringSetupView: View {
6266
}
6367

6468
if !adbAvailable || !scrcpyAvailable || !mediaControlAvailable {
65-
Text("Install with Homebrew by running the following commands in Terminal:")
66-
.font(.body)
67-
.foregroundStyle(.secondary)
68-
.multilineTextAlignment(.center)
69-
.padding(.horizontal)
70-
.frame(maxWidth: 500)
71-
72-
VStack(alignment: .leading, spacing: 8) {
73-
if !adbAvailable {
74-
commandRow("brew install android-platform-tools")
69+
// If ALL three missing and brew is not available, guide to Homebrew website.
70+
if !brewAvailable && !adbAvailable && !scrcpyAvailable && !mediaControlAvailable {
71+
VStack(spacing: 12) {
72+
Text("Homebrew not found")
73+
.font(.headline)
74+
Text("To install ADB, scrcpy, and media-control, you need Homebrew. Click below to open brew.sh and follow the instructions. Then return here and tap 'Check Again'.")
75+
.font(.body)
76+
.foregroundStyle(.secondary)
77+
.multilineTextAlignment(.center)
78+
.frame(maxWidth: 520)
79+
80+
GlassButtonView(
81+
label: "Get Homebrew",
82+
systemImage: "safari",
83+
size: .large,
84+
primary: true,
85+
action: {
86+
if let url = URL(string: "https://brew.sh") {
87+
NSWorkspace.shared.open(url)
88+
}
89+
}
90+
)
91+
92+
GlassButtonView(
93+
label: "Check Again",
94+
systemImage: "arrow.clockwise",
95+
size: .large,
96+
action: {
97+
checking = true
98+
checkDependencies()
99+
}
100+
)
75101
}
76-
if !scrcpyAvailable {
77-
commandRow("brew install scrcpy")
102+
.transition(.identity)
103+
} else {
104+
Text("Install with Homebrew or copy these commands:")
105+
.font(.body)
106+
.foregroundStyle(.secondary)
107+
.multilineTextAlignment(.center)
108+
.padding(.horizontal)
109+
.frame(maxWidth: 500)
110+
111+
VStack(alignment: .leading, spacing: 10) {
112+
if !adbAvailable {
113+
installRow(
114+
title: "android-platform-tools",
115+
command: "brew install android-platform-tools"
116+
)
117+
}
118+
if !scrcpyAvailable {
119+
installRow(
120+
title: "scrcpy",
121+
command: "brew install scrcpy"
122+
)
123+
}
124+
if !mediaControlAvailable {
125+
installRow(
126+
title: "media-control",
127+
command: "brew install media-control"
128+
)
129+
}
78130
}
79-
if !mediaControlAvailable {
80-
commandRow("brew install media-control")
131+
.padding()
132+
.background(Color.secondary.opacity(0.08))
133+
.cornerRadius(10)
134+
135+
if let pkg = installingPackage {
136+
VStack(alignment: .leading, spacing: 8) {
137+
HStack(spacing: 8) {
138+
ProgressView()
139+
Text("Installing \(pkg) via Homebrew…")
140+
}
141+
.font(.callout)
142+
.foregroundStyle(.secondary)
143+
144+
ScrollView {
145+
Text(installLog.isEmpty ? "Running brew install…" : installLog)
146+
.font(.system(.footnote, design: .monospaced))
147+
.frame(maxWidth: .infinity, alignment: .leading)
148+
.textSelection(.enabled)
149+
}
150+
.frame(maxHeight: 140)
151+
.background(Color.black.opacity(0.05))
152+
.cornerRadius(8)
153+
}
154+
.transition(.identity)
81155
}
156+
157+
GlassButtonView(
158+
label: "Check Again",
159+
systemImage: "arrow.clockwise",
160+
size: .large,
161+
action: {
162+
checking = true
163+
checkDependencies()
164+
}
165+
)
166+
.disabled(installingPackage != nil)
167+
.transition(.identity)
82168
}
83-
.padding()
84-
.background(Color.secondary.opacity(0.1))
85-
.cornerRadius(8)
86-
87-
Text("After installing, click 'Check Again' to verify.")
88-
.font(.footnote)
89-
.foregroundStyle(.secondary)
90-
91-
GlassButtonView(
92-
label: "Check Again",
93-
systemImage: "arrow.clockwise",
94-
size: .large,
95-
action: {
96-
checking = true
97-
checkDependencies()
98-
}
99-
)
100-
.transition(.identity)
101169
} else {
102170
Text("Great! ADB, scrcpy, and media-control are available.")
103171
.font(.callout)
@@ -136,18 +204,50 @@ struct MirroringSetupView: View {
136204
let adbFound = ADBConnector.findExecutable(named: "adb", fallbackPaths: ADBConnector.possibleADBPaths) != nil
137205
let scrcpyFound = ADBConnector.findExecutable(named: "scrcpy", fallbackPaths: ADBConnector.possibleScrcpyPaths) != nil
138206
let mediaFound = ADBConnector.findExecutable(named: "media-control", fallbackPaths: ["/opt/homebrew/bin/media-control", "/usr/local/bin/media-control"]) != nil
207+
let brewFound = ADBConnector.findExecutable(named: "brew", fallbackPaths: ["/opt/homebrew/bin/brew", "/usr/local/bin/brew"]) != nil
139208

140209
// let scrcpyFound = false
141210

142211
DispatchQueue.main.async {
143212
self.adbAvailable = adbFound
144213
self.scrcpyAvailable = scrcpyFound
145214
self.mediaControlAvailable = mediaFound
215+
self.brewAvailable = brewFound
146216
self.checking = false
147217
}
148218
}
149219
}
150220

221+
@ViewBuilder
222+
private func installRow(title: String, command: String) -> some View {
223+
VStack(alignment: .leading, spacing: 6) {
224+
HStack(alignment: .firstTextBaseline, spacing: 10) {
225+
Text(command)
226+
.font(.system(.body, design: .monospaced))
227+
.foregroundColor(.primary)
228+
.textSelection(.enabled)
229+
Spacer()
230+
if brewAvailable {
231+
GlassButtonView(
232+
label: "Install",
233+
systemImage: "square.and.arrow.down",
234+
action: {
235+
runBrewInstall(for: title)
236+
}
237+
)
238+
.disabled(installingPackage != nil)
239+
}
240+
Button(action: {
241+
copyToClipboard(command)
242+
}) {
243+
Image(systemName: "doc.on.doc")
244+
.foregroundColor(.secondary)
245+
}
246+
.buttonStyle(.plain)
247+
}
248+
}
249+
}
250+
151251
@ViewBuilder
152252
private func commandRow(_ command: String) -> some View {
153253
HStack {
@@ -170,6 +270,59 @@ struct MirroringSetupView: View {
170270
pasteboard.clearContents()
171271
pasteboard.setString(text, forType: .string)
172272
}
273+
274+
private func runBrewInstall(for formula: String) {
275+
guard installingPackage == nil else { return }
276+
installingPackage = formula
277+
installLog = ""
278+
279+
DispatchQueue.global(qos: .userInitiated).async {
280+
// Find brew
281+
guard let brewPath = ADBConnector.findExecutable(named: "brew", fallbackPaths: ["/opt/homebrew/bin/brew", "/usr/local/bin/brew"]) else {
282+
DispatchQueue.main.async {
283+
self.brewAvailable = false
284+
self.installingPackage = nil
285+
}
286+
return
287+
}
288+
289+
let process = Process()
290+
process.executableURL = URL(fileURLWithPath: brewPath)
291+
process.arguments = ["install", formula]
292+
293+
let pipe = Pipe()
294+
process.standardOutput = pipe
295+
process.standardError = pipe
296+
297+
// Stream logs to UI
298+
pipe.fileHandleForReading.readabilityHandler = { handle in
299+
let data = handle.availableData
300+
guard !data.isEmpty, let chunk = String(data: data, encoding: .utf8), !chunk.isEmpty else { return }
301+
DispatchQueue.main.async {
302+
self.installLog += chunk
303+
}
304+
}
305+
306+
process.terminationHandler = { _ in
307+
pipe.fileHandleForReading.readabilityHandler = nil
308+
DispatchQueue.main.async {
309+
self.installingPackage = nil
310+
self.checking = true
311+
}
312+
// Re-check tools after install
313+
self.checkDependencies()
314+
}
315+
316+
do {
317+
try process.run()
318+
} catch {
319+
DispatchQueue.main.async {
320+
self.installLog += "\nFailed to run brew install: \(error.localizedDescription)"
321+
self.installingPackage = nil
322+
}
323+
}
324+
}
325+
}
173326
}
174327

175328
#Preview {

0 commit comments

Comments
 (0)