package updater import ( "bufio" "context" "os/exec" "strings" ) type Package struct { Source string Name string Current string Available string Ignored bool } type Result struct { Packages []Package Warnings []string } type Options struct { CheckAUR bool IgnoredPackages []string } func (r Result) Total() int { total := 0 for _, pkg := range r.Packages { if !pkg.Ignored { total++ } } return total } func (r Result) IgnoredTotal() int { total := 0 for _, pkg := range r.Packages { if pkg.Ignored { total++ } } return total } 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...) } markIgnored(&result, opts.IgnoredPackages) return result, nil } func markIgnored(result *Result, names []string) { ignored := map[string]bool{} for _, name := range names { name = strings.TrimSpace(name) if name != "" { ignored[name] = true } } if len(ignored) == 0 { return } for i := range result.Packages { result.Packages[i].Ignored = ignored[result.Packages[i].Name] } } 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:]) }