66//
77
88import SwiftUI
9+ import AppKit
910
1011struct 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 += " \n Failed to run brew install: \( error. localizedDescription) "
321+ self . installingPackage = nil
322+ }
323+ }
324+ }
325+ }
173326}
174327
175328#Preview {
0 commit comments