package updater import ( "bufio" "context" "os/exec" "strings" ) type Package struct { Source string Name string Current string Available string } type Result struct { Packages []Package Warnings []string } type Options struct { CheckAUR bool } func (r Result) Total() int { return len(r.Packages) } func (r Result) Summary() string { total := r.Total() switch total { case 0: return "No updates available." case 1: return "1 update available." default: return strings.TrimSpace(strings.Join([]string{itoa(total), "updates available."}, " ")) } } func Check(ctx context.Context) (Result, error) { return CheckWithOptions(ctx, Options{CheckAUR: true}) } func CheckWithOptions(ctx context.Context, opts Options) (Result, error) { var result Result pacman, err := checkPacman(ctx) if err != nil { result.Warnings = append(result.Warnings, err.Error()) } result.Packages = append(result.Packages, pacman...) if opts.CheckAUR { aur, err := checkAUR(ctx) if err != nil { result.Warnings = append(result.Warnings, err.Error()) } result.Packages = append(result.Packages, aur...) } return result, nil } func checkPacman(ctx context.Context) ([]Package, error) { if _, err := exec.LookPath("checkupdates"); err == nil { out, err := exec.CommandContext(ctx, "checkupdates").Output() if err != nil { return nil, nil } return parseUpdates("pacman", string(out)), nil } out, err := exec.CommandContext(ctx, "pacman", "-Qu").Output() if err != nil { return nil, nil } return parseUpdates("pacman", string(out)), nil } func checkAUR(ctx context.Context) ([]Package, error) { helper := AURHelper() if helper == "" { return nil, nil } out, err := exec.CommandContext(ctx, helper, "-Qua").Output() if err != nil { return nil, nil } return parseUpdates("aur", string(out)), nil } func AURHelper() string { for _, name := range []string{"paru", "yay"} { if _, err := exec.LookPath(name); err == nil { return name } } return "" } func parseUpdates(source, output string) []Package { var packages []Package scanner := bufio.NewScanner(strings.NewReader(output)) for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) if line == "" { continue } packages = append(packages, parseLine(source, line)) } return packages } func parseLine(source, line string) Package { fields := strings.Fields(line) pkg := Package{Source: source} if len(fields) == 0 { return pkg } pkg.Name = fields[0] if len(fields) >= 4 && fields[2] == "->" { pkg.Current = fields[1] pkg.Available = fields[3] return pkg } if len(fields) >= 3 && fields[1] == "->" { pkg.Available = fields[2] return pkg } return pkg } func itoa(n int) string { if n == 0 { return "0" } var buf [20]byte i := len(buf) for n > 0 { i-- buf[i] = byte('0' + n%10) n /= 10 } return string(buf[i:]) }