diff --git a/internal/providers/archlinuxpkgs/README.md b/internal/providers/archlinuxpkgs/README.md index 51d190cb..b7ea0e1b 100755 --- a/internal/providers/archlinuxpkgs/README.md +++ b/internal/providers/archlinuxpkgs/README.md @@ -7,10 +7,12 @@ Find, install and delete packages. Including AUR. - find official packages - find AUR packages - install packages -- list all exclusively installed packages +- list installed packages (explicitly or not). +- detect and update outdated packages. - remove packages - clear all done items #### Requirements - `yay` or `paru` for AUR +- [`checkupadtes`](https://man.archlinux.org/man/extra/pacman-contrib/checkupdates.8.en) (optional) to safely check for official repository updates without requiring a database sync. diff --git a/internal/providers/archlinuxpkgs/package.go b/internal/providers/archlinuxpkgs/package.go index 3ca6a801..e2b49266 100644 --- a/internal/providers/archlinuxpkgs/package.go +++ b/internal/providers/archlinuxpkgs/package.go @@ -18,6 +18,7 @@ type Package struct { Repository string Version string Installed bool + Outdated bool FullInfo string URL string URLPath string diff --git a/internal/providers/archlinuxpkgs/package_gen.go b/internal/providers/archlinuxpkgs/package_gen.go index 11e3cdae..a183c089 100644 --- a/internal/providers/archlinuxpkgs/package_gen.go +++ b/internal/providers/archlinuxpkgs/package_gen.go @@ -227,6 +227,12 @@ func (z *Package) DecodeMsg(dc *msgp.Reader) (err error) { err = msgp.WrapError(err, "Installed") return } + case "Outdated": + z.Outdated, err = dc.ReadBool() + if err != nil { + err = msgp.WrapError(err, "Outdated") + return + } case "FullInfo": z.FullInfo, err = dc.ReadString() if err != nil { @@ -294,9 +300,9 @@ func (z *Package) DecodeMsg(dc *msgp.Reader) (err error) { // EncodeMsg implements msgp.Encodable func (z *Package) EncodeMsg(en *msgp.Writer) (err error) { - // map header, size 14 + // map header, size 15 // write "Name" - err = en.Append(0x8e, 0xa4, 0x4e, 0x61, 0x6d, 0x65) + err = en.Append(0x8f, 0xa4, 0x4e, 0x61, 0x6d, 0x65) if err != nil { return } @@ -345,6 +351,16 @@ func (z *Package) EncodeMsg(en *msgp.Writer) (err error) { err = msgp.WrapError(err, "Installed") return } + // write "Outdated" + err = en.Append(0xa8, 0x4f, 0x75, 0x74, 0x64, 0x61, 0x74, 0x65, 0x64) + if err != nil { + return + } + err = en.WriteBool(z.Outdated) + if err != nil { + err = msgp.WrapError(err, "Outdated") + return + } // write "FullInfo" err = en.Append(0xa8, 0x46, 0x75, 0x6c, 0x6c, 0x49, 0x6e, 0x66, 0x6f) if err != nil { @@ -441,9 +457,9 @@ func (z *Package) EncodeMsg(en *msgp.Writer) (err error) { // MarshalMsg implements msgp.Marshaler func (z *Package) MarshalMsg(b []byte) (o []byte, err error) { o = msgp.Require(b, z.Msgsize()) - // map header, size 14 + // map header, size 15 // string "Name" - o = append(o, 0x8e, 0xa4, 0x4e, 0x61, 0x6d, 0x65) + o = append(o, 0x8f, 0xa4, 0x4e, 0x61, 0x6d, 0x65) o = msgp.AppendString(o, z.Name) // string "Description" o = append(o, 0xab, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e) @@ -457,6 +473,9 @@ func (z *Package) MarshalMsg(b []byte) (o []byte, err error) { // string "Installed" o = append(o, 0xa9, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x65, 0x64) o = msgp.AppendBool(o, z.Installed) + // string "Outdated" + o = append(o, 0xa8, 0x4f, 0x75, 0x74, 0x64, 0x61, 0x74, 0x65, 0x64) + o = msgp.AppendBool(o, z.Outdated) // string "FullInfo" o = append(o, 0xa8, 0x46, 0x75, 0x6c, 0x6c, 0x49, 0x6e, 0x66, 0x6f) o = msgp.AppendString(o, z.FullInfo) @@ -535,6 +554,12 @@ func (z *Package) UnmarshalMsg(bts []byte) (o []byte, err error) { err = msgp.WrapError(err, "Installed") return } + case "Outdated": + z.Outdated, bts, err = msgp.ReadBoolBytes(bts) + if err != nil { + err = msgp.WrapError(err, "Outdated") + return + } case "FullInfo": z.FullInfo, bts, err = msgp.ReadStringBytes(bts) if err != nil { @@ -603,6 +628,6 @@ func (z *Package) UnmarshalMsg(bts []byte) (o []byte, err error) { // Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message func (z *Package) Msgsize() (s int) { - s = 1 + 5 + msgp.StringPrefixSize + len(z.Name) + 12 + msgp.StringPrefixSize + len(z.Description) + 11 + msgp.StringPrefixSize + len(z.Repository) + 8 + msgp.StringPrefixSize + len(z.Version) + 10 + msgp.BoolSize + 9 + msgp.StringPrefixSize + len(z.FullInfo) + 4 + msgp.StringPrefixSize + len(z.URL) + 8 + msgp.StringPrefixSize + len(z.URLPath) + 11 + msgp.StringPrefixSize + len(z.Maintainer) + 10 + msgp.StringPrefixSize + len(z.Submitter) + 9 + msgp.IntSize + 11 + msgp.Float64Size + 15 + msgp.Int64Size + 13 + msgp.Int64Size + s = 1 + 5 + msgp.StringPrefixSize + len(z.Name) + 12 + msgp.StringPrefixSize + len(z.Description) + 11 + msgp.StringPrefixSize + len(z.Repository) + 8 + msgp.StringPrefixSize + len(z.Version) + 10 + msgp.BoolSize + 9 + msgp.BoolSize + 9 + msgp.StringPrefixSize + len(z.FullInfo) + 4 + msgp.StringPrefixSize + len(z.URL) + 8 + msgp.StringPrefixSize + len(z.URLPath) + 11 + msgp.StringPrefixSize + len(z.Maintainer) + 10 + msgp.StringPrefixSize + len(z.Submitter) + 9 + msgp.IntSize + 11 + msgp.Float64Size + 15 + msgp.Int64Size + 13 + msgp.Int64Size return } diff --git a/internal/providers/archlinuxpkgs/setup.go b/internal/providers/archlinuxpkgs/setup.go index fae09786..b8907d21 100755 --- a/internal/providers/archlinuxpkgs/setup.go +++ b/internal/providers/archlinuxpkgs/setup.go @@ -22,12 +22,21 @@ import ( "github.com/tinylib/msgp/msgp" ) +type Filter int + +const ( + All Filter = iota + Installed + Outdated +) + var ( Name = "archlinuxpkgs" NamePretty = "Arch Linux Packages" config *Config installed = []string{} - installedOnly = false + outdated = []string{} + filter Filter = All cacheFile = common.CacheFile("archlinuxpkgs.json") cachedData = newCachedData() ) @@ -41,7 +50,9 @@ const ( ActionVisitURL = "visit_url" ActionRefresh = "refresh" ActionRemove = "remove" + ActionUpdate = "update" ActionShowInstalled = "show_installed" + ActionShowOutdated = "show_outdated" ActionShowAll = "show_all" ) @@ -49,7 +60,9 @@ type Config struct { common.Config `koanf:",squash"` CommandInstall string `koanf:"command_install" desc:"default command for AUR packages to install. supports %VALUE%." default:"yay -S %VALUE%"` CommandRemove string `koanf:"command_remove" desc:"default command to remove packages. supports %VALUE%." default:"sudo pacman -R %VALUE%"` + CommandUpdate string `koanf:"command_update" desc:"default command to update outdated packages." default:"yay -Suy"` AutoWrapWithTerminal bool `koanf:"auto_wrap_with_terminal" desc:"automatically wraps the command with terminal" default:"true"` + ExplicitOnly bool `koanf:"explicit_only" desc:"when filtering installed packages, show only explicitly installed packages, not dependencies" default:"true"` } type AURPackage struct { @@ -123,7 +136,9 @@ func LoadConfig() { }, CommandInstall: fmt.Sprintf("%s -S %s", helper, "%VALUE%"), CommandRemove: fmt.Sprintf("%s -R %s", helper, "%VALUE%"), + CommandUpdate: fmt.Sprintf("%s -Suy", helper), AutoWrapWithTerminal: true, + ExplicitOnly: true, } common.LoadConfig(Name, config) @@ -142,6 +157,7 @@ func Setup() { func setup() { getInstalled() + getOutdated() getOfficialPkgs() setupAURPkgs() @@ -196,11 +212,14 @@ func Activate(single bool, identifier, action string, query string, args string, setup() return case ActionShowAll: - installedOnly = false + filter = All return case ActionShowInstalled: - installedOnly = true + filter = Installed return + case ActionShowOutdated: + filter = Outdated + return } name := cachedData.Packages[identifier].Name @@ -211,6 +230,8 @@ func Activate(single bool, identifier, action string, query string, args string, pkgcmd = config.CommandInstall case ActionRemove: pkgcmd = config.CommandRemove + case ActionUpdate: + pkgcmd = config.CommandUpdate default: slog.Error(Name, "activate", fmt.Sprintf("unknown action: %s", action)) return @@ -249,9 +270,9 @@ func Query(conn net.Conn, query string, single bool, exact bool, _ uint8) []*pb. } for k, v := range cachedData.Packages { - if installedOnly && !v.Installed { - continue - } + if (filter == Installed && !v.Installed) || (filter == Outdated && !v.Outdated) { + continue + } state := []string{} a := []string{} @@ -268,9 +289,15 @@ func Query(conn net.Conn, query string, single bool, exact bool, _ uint8) []*pb. a = append(a, "visit_url") } - subtext := fmt.Sprintf("[%s]", strings.ToLower(v.Repository)) - if v.Installed { - subtext = fmt.Sprintf("[%s] [installed]", strings.ToLower(v.Repository)) + repo := fmt.Sprintf("[%s]", strings.ToLower(v.Repository)) + var subtext string + switch { + case v.Installed && v.Outdated: + subtext = repo + " [installed] [outdated]" + case v.Installed: + subtext = repo + " [installed]" + default: + subtext = repo } e := &pb.QueryResponse_Item{ @@ -320,17 +347,28 @@ func HideFromProviderlist() bool { } func State(provider string) *pb.ProviderStateResponse { - actions := []string{ActionRefresh} + actions := []string{ActionRefresh} - if installedOnly { + outdatedIsEmpty := len(outdated) != 0 + + switch filter { + case Installed: actions = append(actions, ActionShowAll) - } else { + case Outdated: + actions = append(actions, ActionShowInstalled, ActionShowAll) + default: // All actions = append(actions, ActionShowInstalled) } - - return &pb.ProviderStateResponse{ - Actions: actions, + if outdatedIsEmpty && filter != Outdated { + actions = append(actions, ActionShowOutdated) + } + if outdatedIsEmpty { + actions = append(actions, ActionUpdate) } + + return &pb.ProviderStateResponse{ + Actions: actions, + } } func getOfficialPkgs() { @@ -363,6 +401,7 @@ func getOfficialPkgs() { case strings.HasPrefix(line, "Name"): e.Name = strings.TrimSpace(strings.Split(line, ":")[1]) e.Installed = slices.Contains(installed, e.Name) + e.Outdated = slices.Contains(outdated, e.Name) case strings.HasPrefix(line, "Description"): e.Description = strings.TrimSpace(strings.Split(line, ":")[1]) case strings.HasPrefix(line, "Version"): @@ -412,6 +451,7 @@ func setupAURPkgs() { Version: pkg.Version, Repository: "aur", Installed: slices.Contains(installed, pkg.Name), + Outdated: slices.Contains(outdated, pkg.Name), URL: pkg.URL, FullInfo: pkg.toFullInfo(), } @@ -420,8 +460,15 @@ func setupAURPkgs() { func getInstalled() { installed = []string{} + + var cmd *exec.Cmd + + if config.ExplicitOnly { + cmd = exec.Command("pacman", "-Qe") + } else { + cmd = exec.Command("pacman", "-Q") + } - cmd := exec.Command("pacman", "-Qe") out, err := cmd.CombinedOutput() if err != nil { slog.Error(Name, "installed", err) @@ -434,3 +481,53 @@ func getInstalled() { } } } + +func getOutdated() { + outdated = []string{} + + var offCmd *exec.Cmd + if _, err := exec.LookPath("checkupdates"); err == nil { + offCmd = exec.Command("checkupdates") + } else { + offCmd = exec.Command("pacman", "-Qu") + } + + offOut, offErr := offCmd.CombinedOutput() + if offErr != nil { + // checkupdates return 2 when no updates are required + if exitErr, ok := offErr.(*exec.ExitError); ok && exitErr.ExitCode() != 2 { + slog.Error(Name, "outdated", offErr) + } + } + + + aurCmd := exec.Command(detectHelper(), "-Qu", "--aur") + aurOut, aurErr := aurCmd.CombinedOutput() + if aurErr != nil { + // yay -Qu --aur return 1 when no updates are required + if exitErr, ok := aurErr.(*exec.ExitError); ok && exitErr.ExitCode() != 1 { + slog.Error(Name, "outdated", offErr) + } + } + + var installedSet map[string]struct{} + if config.ExplicitOnly { + installedSet = make(map[string]struct{}, len(installed)) + for _, pkg := range installed { + installedSet[pkg] = struct{}{} + } + } + + for _, out := range [][]byte{offOut, aurOut} { + for line := range strings.Lines(string(out)) { + fields := strings.Fields(line) + if len(fields) > 0 { + name := fields[0] + _, inInstalled := installedSet[name] + if !config.ExplicitOnly || inInstalled { + outdated = append(outdated, name) + } + } + } + } +}