11package macup
22
33import (
4+ "encoding/json"
45 "fmt"
56 "io"
67 "net/http"
78 "os"
89 "os/exec"
10+ "path/filepath"
911 "strings"
12+ "sync"
1013 "time"
14+
15+ "github.com/AlecAivazis/survey/v2"
1116)
1217
1318// Terminal color codes and constants
@@ -19,39 +24,50 @@ const (
1924 k_timeout = 5 * time .Second // Timeout for HTTP requests
2025 k_testURL = "https://www.google.com" // URL to test internet connection
2126 k_gemCmdPath = "/usr/bin/gem" // Path to the gem command
27+ k_configFile = ".macup.json" // Configuration file name
2228)
2329
24- // Print a message in green color with a newline
30+ // Update represents a single update function.
31+ type Update struct {
32+ Name string
33+ Description string
34+ Run func (writer io.Writer )
35+ }
36+
37+ // Config represents the user's selections.
38+ type Config struct {
39+ SelectedUpdates []string `json:"selected_updates"`
40+ }
41+
42+ // printlnGreen prints a message in green color with a newline.
2543func printlnGreen (writer io.Writer , msg string ) {
2644 fmt .Fprintf (writer , "\n %s%s%s\n " , k_green , msg , k_clear )
2745}
2846
29- // Print a message in yellow color (no newline)
47+ // printlnYellow prints a message in yellow color (no newline).
3048func printlnYellow (writer io.Writer , msg string ) {
3149 fmt .Fprintf (writer , "%s%s%s" , k_yellow , msg , k_clear )
3250}
3351
34- // Check if a command exists in `PATH`, print warning if not
52+ // checkCommand checks if a command exists in `PATH`, print warning if not.
3553func checkCommand (writer io.Writer , cmd string ) bool {
3654 _ , err := exec .LookPath (cmd )
37-
3855 if err != nil {
3956 printlnYellow (writer , cmd + " is not installed." )
4057 return false
4158 }
42-
4359 return true
4460}
4561
46- // Run a shell command and direct its output to writer
62+ // runCommand runs a shell command and directs its output to writer.
4763func runCommand (writer io.Writer , name string , args ... string ) {
4864 cmd := exec .Command (name , args ... )
4965 cmd .Stdout = writer
5066 cmd .Stderr = writer
5167 cmd .Run ()
5268}
5369
54- // Update Homebrew formulas and perform diagnostics
70+ // UpdateBrew updates Homebrew formulas and perform diagnostics.
5571func UpdateBrew (writer io.Writer ) {
5672 printlnGreen (writer , "Updating Brew Formulas" )
5773 if checkCommand (writer , "brew" ) {
@@ -64,15 +80,15 @@ func UpdateBrew(writer io.Writer) {
6480 }
6581}
6682
67- // Update VSCode extensions
83+ // UpdateVSCodeExt updates VSCode extensions.
6884func UpdateVSCodeExt (writer io.Writer ) {
6985 printlnGreen (writer , "Updating VSCode Extensions" )
7086 if checkCommand (writer , "code" ) {
7187 runCommand (writer , "code" , "--update-extensions" )
7288 }
7389}
7490
75- // Update Ruby gems and clean up
91+ // UpdateGem updates Ruby gems and clean up.
7692func UpdateGem (writer io.Writer ) {
7793 printlnGreen (writer , "Updating Gems" )
7894 gemPath , err := exec .LookPath ("gem" )
@@ -84,7 +100,7 @@ func UpdateGem(writer io.Writer) {
84100 runCommand (writer , "gem" , "cleanup" , "--user-install" )
85101}
86102
87- // Update global Node.js, npm, and Yarn packages
103+ // UpdateNodePkg updates global Node.js, npm, and Yarn packages.
88104func UpdateNodePkg (writer io.Writer ) {
89105 printlnGreen (writer , "Updating Node Packages" )
90106 if checkCommand (writer , "node" ) {
@@ -100,13 +116,13 @@ func UpdateNodePkg(writer io.Writer) {
100116 }
101117}
102118
103- // Update Rust Cargo crates by reinstalling each listed crate
119+ // UpdateCargo updates Rust Cargo crates by reinstalling each listed crate.
104120func UpdateCargo (writer io.Writer ) {
105121 printlnGreen (writer , "Updating Rust Cargo Crates" )
106122 if checkCommand (writer , "cargo" ) {
107123 out , _ := exec .Command ("cargo" , "install" , "--list" ).Output ()
108- lines := strings .SplitSeq (string (out ), "\n " )
109- for line := range lines {
124+ lines := strings .Split (string (out ), "\n " )
125+ for _ , line := range lines {
110126 if fields := strings .Fields (line ); len (fields ) > 0 {
111127 name := fields [0 ]
112128 runCommand (writer , "cargo" , "install" , name )
@@ -115,28 +131,27 @@ func UpdateCargo(writer io.Writer) {
115131 }
116132}
117133
118- // Update Mac App Store applications
134+ // UpdateAppStore updates Mac App Store applications.
119135func UpdateAppStore (writer io.Writer ) {
120136 printlnGreen (writer , "Updating App Store Applications" )
121137 if checkCommand (writer , "mas" ) {
122138 runCommand (writer , "mas" , "upgrade" )
123139 }
124140}
125141
126- // Update macOS system software
142+ // UpdateMacOS updates macOS system software.
127143func UpdateMacOS (writer io.Writer ) {
128144 printlnGreen (writer , "Updating MacOS" )
129145 runCommand (writer , "softwareupdate" , "-i" , "-a" )
130146}
131147
132- // Check for internet connectivity by making an HTTP request
148+ // CheckInternet checks for internet connectivity by making an HTTP request.
133149func CheckInternet () bool {
134150 client := http.Client {
135151 Timeout : k_timeout ,
136152 }
137153
138154 resp , err := client .Get (k_testURL )
139-
140155 if err != nil {
141156 fmt .Fprintf (os .Stderr , "\n %s%s%s\n " , k_red , "⚠️ No Internet Connection!!!" , k_clear )
142157 return false
@@ -145,3 +160,113 @@ func CheckInternet() bool {
145160
146161 return resp .StatusCode == http .StatusOK
147162}
163+
164+ // homeDir returns the user's home directory.
165+ func homeDir () string {
166+ home , err := os .UserHomeDir ()
167+ if err != nil {
168+ fmt .Fprintf (os .Stderr , "Error getting home directory: %v\n " , err )
169+ os .Exit (1 )
170+ }
171+ return home
172+ }
173+
174+ // configPath returns the full path to the configuration file.
175+ func configPath () string {
176+ return filepath .Join (homeDir (), k_configFile )
177+ }
178+
179+ // LoadConfig loads the user's selections from the configuration file.
180+ func LoadConfig () (* Config , error ) {
181+ path := configPath ()
182+ data , err := os .ReadFile (path )
183+ if err != nil {
184+ if os .IsNotExist (err ) {
185+ return & Config {}, nil
186+ }
187+ return nil , err
188+ }
189+
190+ var config Config
191+ if err := json .Unmarshal (data , & config ); err != nil {
192+ return nil , err
193+ }
194+ return & config , nil
195+ }
196+
197+ // SaveConfig saves the user's selections to the configuration file.
198+ func (c * Config ) SaveConfig () error {
199+ path := configPath ()
200+ data , err := json .MarshalIndent (c , "" , " " )
201+ if err != nil {
202+ return err
203+ }
204+ return os .WriteFile (path , data , 0644 )
205+ }
206+
207+ // Updates is a list of all available update functions.
208+ var Updates = []Update {
209+ {"brew" , "Update Homebrew packages" , UpdateBrew },
210+ {"vscode" , "Update VSCode extensions" , UpdateVSCodeExt },
211+ {"gem" , "Update Ruby gems" , UpdateGem },
212+ {"node" , "Update Node.js packages" , UpdateNodePkg },
213+ {"cargo" , "Update Rust packages" , UpdateCargo },
214+ {"appstore" , "Update Mac App Store apps" , UpdateAppStore },
215+ {"macos" , "Update macOS system" , UpdateMacOS },
216+ }
217+
218+ // Run runs the selected update functions.
219+ func Run (writer io.Writer , selectedUpdates []string ) {
220+ var wg sync.WaitGroup
221+ for _ , updateName := range selectedUpdates {
222+ for _ , u := range Updates {
223+ if u .Name == updateName {
224+ wg .Add (1 )
225+ go func (u Update ) {
226+ defer wg .Done ()
227+ u .Run (writer )
228+ }(u )
229+ }
230+ }
231+ }
232+ wg .Wait ()
233+ }
234+
235+ // SelectUpdates prompts the user to select which updates to run.
236+ func SelectUpdates (config * Config ) ([]string , error ) {
237+ options := make ([]string , len (Updates ))
238+ for i , u := range Updates {
239+ options [i ] = u .Description
240+ }
241+
242+ defaults := []string {}
243+ for _ , s := range config .SelectedUpdates {
244+ for _ , u := range Updates {
245+ if u .Name == s {
246+ defaults = append (defaults , u .Description )
247+ }
248+ }
249+ }
250+
251+ prompt := & survey.MultiSelect {
252+ Message : "Select the updates you want to run:" ,
253+ Options : options ,
254+ Default : defaults ,
255+ }
256+
257+ var selectedDescriptions []string
258+ if err := survey .AskOne (prompt , & selectedDescriptions ); err != nil {
259+ return nil , err
260+ }
261+
262+ selectedNames := []string {}
263+ for _ , desc := range selectedDescriptions {
264+ for _ , u := range Updates {
265+ if u .Description == desc {
266+ selectedNames = append (selectedNames , u .Name )
267+ }
268+ }
269+ }
270+
271+ return selectedNames , nil
272+ }
0 commit comments