diff options
| -rw-r--r-- | brewget.go | 101 | ||||
| -rw-r--r-- | cache.go | 2 | ||||
| -rw-r--r-- | main.go | 103 |
3 files changed, 195 insertions, 11 deletions
@@ -2,9 +2,11 @@ package main import ( "encoding/json" - "github.com/fatih/color" "os/exec" "strings" + "time" + + "github.com/fatih/color" ) type FormulaInfo struct { @@ -34,6 +36,9 @@ type CaskInfo struct { Version string `json:"version"` } +const brewDepCacheTTL = 3*24*time.Hour +const brewListCacheTTL = 24*time.Hour + // GetInfo fetches package info for both formulae and casks. // It returns one of the two structs depending on the package type. func GetBrewInfo(pkg string) (*FormulaInfo, *CaskInfo) { @@ -76,3 +81,97 @@ func GetBrewUses(pkg string) []string { return lines } + +func GetBrewList() []string { + const cachepath = "brewlist.json" + var list []string + + // Try reading from cache + list_raw, cache_hit := ReadCache(cachepath, brewListCacheTTL) + if cache_hit { + err := json.Unmarshal(list_raw, &list) + if err != nil { + color.Red("❌ Error unmarshaling cache of brew list") + return []string{} + } + } else { + out, err := exec. + Command("brew", "list", "--installed-on-request"). + Output() + if err != nil { + color.Red("❌ Error fetching brew list.") + return []string{} + } + + list = strings.Split(strings.TrimSpace(string(out)), "\n") + } + + // Write cache + defer func() { + brewlist_json, err := json.Marshal(list) + if err != nil { + color.Red("❌ Error marshaling brew list data to write cache.") + } + WriteCache(cachepath, brewlist_json) + }() + + return list +} + +func GetBrewDeps() map[string][]string { + depmap := map[string][]string{} + + // Try reading from cache + const cachepath = "depmap.json" + cache_data, cache_hit := ReadCache(cachepath, brewDepCacheTTL) + + if cache_hit { + // if cache hit, decode + err := json.Unmarshal(cache_data, &depmap) + if err != nil { + color.Red("❌ Error unmarshaling cache of dependency map.") + } + } else { + // if not, try fetch + out, err := exec. + Command("brew", "deps", "--installed", "--direct"). + Output() + if err != nil { + color.Red("❌ Error fetching brew dependencies.") + return map[string][]string{} + } + + // parse raw data + // a list of items like "package: depA depB depC..." + dep_strs := strings.FieldsFunc(string(out), func(r rune) bool { + return r == '\n' || r == '\r' + }) + + for _, dep_str := range dep_strs { + parts := strings.Split(dep_str, ":") + pkg := parts[0] + pkg_deps := parts[1] + + deps := strings.Split(pkg_deps, " ") + filtered := []string{} + // remove empty elements in slice + for _, dep := range deps { + if dep != "" { + filtered = append(filtered, dep) + } + } + depmap[pkg] = filtered + } + } + + // Writing cache + defer func() { + depmap_json, err := json.Marshal(depmap) + if err != nil { + color.Red("Error marshaling JSON when writing cache:", err) + } + WriteCache("depmap.json", depmap_json) + }() + + return depmap +} @@ -14,7 +14,7 @@ func CachePath(rel string) string { return path } -func ReadCache(relPath string, ttl time.Duration) ([]byte, bool) { +func ReadCache(relPath string, ttl time.Duration) (data []byte, cacheHit bool) { path := CachePath(relPath) info, err := os.Stat(path) @@ -3,13 +3,18 @@ package main import ( "context" "fmt" - "github.com/fatih/color" - "github.com/urfave/cli/v3" "log" "os" + "slices" + "sort" + + "github.com/fatih/color" + "github.com/urfave/cli/v3" ) +// flags var NoCache, DoClearCache bool +var ListAscend, ListDescend, ListNoDep bool func main() { EnsureBrewAvailable() @@ -26,13 +31,6 @@ func main() { Value: false, Destination: &NoCache, }, - &cli.BoolFlag{ - Name: "clear-cache", - Aliases: []string{"cc"}, - Usage: "Clear cache", - Value: false, - Destination: &DoClearCache, - }, }, Action: func(ctx context.Context, cmd *cli.Command) error { if DoClearCache { @@ -60,6 +58,93 @@ func main() { return nil }, + Commands: []*cli.Command{ + { + Name: "list", + Aliases: []string{"l", "ls"}, + Usage: "Better brew list.", + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "ascending", + Aliases: []string{"a"}, + Usage: "Sort list ascendingly, based on dependency count.", + Value: false, + Destination: &ListAscend, + }, + &cli.BoolFlag{ + Name: "descending", + Aliases: []string{"d"}, + Usage: "Sort list descendingly, based on dependency count.", + Value: false, + Destination: &ListDescend, + }, + &cli.BoolFlag{ + Name: "no-dependency", + Aliases: []string{"n", "nd"}, + Usage: "Only show packages installed on request, with no dependency list.", + Value: false, + Destination: &ListNoDep, + }, + }, + Action: func(ctx context.Context, cmd *cli.Command) error { + depmap := GetBrewDeps() + list := GetBrewList() + + // filter and get essential items + var keys = []string{} + for k := range depmap { + if !slices.Contains(list, k) { + delete(depmap, k) + } else { + keys = append(keys, k) + } + } + + // sort based on dependency count + if ListAscend { + sort.Slice(keys, func(i, j int) bool { + return len(depmap[keys[i]]) > len(depmap[keys[j]]) + }) + } else if ListDescend { + sort.Slice(keys, func(i, j int) bool { + return len(depmap[keys[i]]) < len(depmap[keys[j]]) + }) + } else { + sort.Strings(keys) + } + + // + for _, pkg := range keys { + deps := depmap[pkg] + count := len(deps) + + color.New(color.FgBlue).Printf("%s", pkg) + if count > 1 { + fmt.Printf(" (%d dependencies)\n", count) + } else { + fmt.Printf(" (%d dependency)\n", count) + } + + if ListNoDep { continue } + for _, dep := range deps { + fmt.Printf(" ► %s\n", dep) + } + } + return nil + }, + }, + { + Name: "clean", + Aliases: []string{"c", "cache"}, + Usage: "Delete all cache files.", + Action: func(ctx context.Context, cmd *cli.Command) error { + // TODO error handling here + ClearCache() + fmt.Println("Cache cleared.") + return nil + }, + }, + }, } // run command and handle error |
