diff --git a/flatpak/capabilities.go b/flatpak/capabilities.go index c63c818..3d74050 100644 --- a/flatpak/capabilities.go +++ b/flatpak/capabilities.go @@ -8,8 +8,9 @@ import ( // Compile-time interface checks. var ( - _ snack.Cleaner = (*Flatpak)(nil) - _ snack.RepoManager = (*Flatpak)(nil) + _ snack.Cleaner = (*Flatpak)(nil) + _ snack.RepoManager = (*Flatpak)(nil) + _ snack.VersionQuerier = (*Flatpak)(nil) ) // Autoremove removes unused runtimes and extensions. @@ -42,3 +43,25 @@ func (f *Flatpak) RemoveRepo(ctx context.Context, id string) error { defer f.Unlock() return removeRepo(ctx, id) } + +// LatestVersion returns the latest available version of a flatpak from +// configured remotes. +func (f *Flatpak) LatestVersion(ctx context.Context, pkg string) (string, error) { + return latestVersion(ctx, pkg) +} + +// ListUpgrades returns flatpaks that have newer versions available. +func (f *Flatpak) ListUpgrades(ctx context.Context) ([]snack.Package, error) { + return listUpgrades(ctx) +} + +// UpgradeAvailable reports whether a newer version is available. +func (f *Flatpak) UpgradeAvailable(ctx context.Context, pkg string) (bool, error) { + return upgradeAvailable(ctx, pkg) +} + +// VersionCmp compares two version strings using basic semver comparison. +// Flatpak has no native version comparison tool. +func (f *Flatpak) VersionCmp(ctx context.Context, ver1, ver2 string) (int, error) { + return versionCmp(ctx, ver1, ver2) +} diff --git a/flatpak/capabilities_linux.go b/flatpak/capabilities_linux.go new file mode 100644 index 0000000..14ef8b0 --- /dev/null +++ b/flatpak/capabilities_linux.go @@ -0,0 +1,56 @@ +//go:build linux + +package flatpak + +import ( + "context" + "fmt" + "strings" + + "github.com/gogrlx/snack" +) + +func latestVersion(ctx context.Context, pkg string) (string, error) { + out, err := run(ctx, []string{"remote-info", "flathub", pkg}) + if err != nil { + if strings.Contains(err.Error(), "exit status 1") { + return "", fmt.Errorf("flatpak latestVersion %s: %w", pkg, snack.ErrNotFound) + } + return "", fmt.Errorf("flatpak latestVersion: %w", err) + } + // remote-info output is key:value like `flatpak info` + p := parseInfo(out) + if p == nil || p.Version == "" { + return "", fmt.Errorf("flatpak latestVersion %s: %w", pkg, snack.ErrNotFound) + } + return p.Version, nil +} + +func listUpgrades(ctx context.Context) ([]snack.Package, error) { + out, err := run(ctx, []string{"remote-ls", "--updates", "--columns=name,application,version,origin"}) + if err != nil { + // No updates available may produce an error on some versions + if strings.Contains(err.Error(), "No updates") { + return nil, nil + } + return nil, fmt.Errorf("flatpak listUpgrades: %w", err) + } + return parseList(out), nil +} + +func upgradeAvailable(ctx context.Context, pkg string) (bool, error) { + upgrades, err := listUpgrades(ctx) + if err != nil { + return false, err + } + for _, u := range upgrades { + if u.Name == pkg || u.Description == pkg { + return true, nil + } + } + return false, nil +} + +func versionCmp(_ context.Context, ver1, ver2 string) (int, error) { + return semverCmp(ver1, ver2), nil +} diff --git a/flatpak/capabilities_other.go b/flatpak/capabilities_other.go new file mode 100644 index 0000000..9dadcec --- /dev/null +++ b/flatpak/capabilities_other.go @@ -0,0 +1,25 @@ +//go:build !linux + +package flatpak + +import ( + "context" + + "github.com/gogrlx/snack" +) + +func latestVersion(_ context.Context, _ string) (string, error) { + return "", snack.ErrUnsupportedPlatform +} + +func listUpgrades(_ context.Context) ([]snack.Package, error) { + return nil, snack.ErrUnsupportedPlatform +} + +func upgradeAvailable(_ context.Context, _ string) (bool, error) { + return false, snack.ErrUnsupportedPlatform +} + +func versionCmp(_ context.Context, _, _ string) (int, error) { + return 0, snack.ErrUnsupportedPlatform +} diff --git a/flatpak/parse.go b/flatpak/parse.go index ff0f36d..a54aece 100644 --- a/flatpak/parse.go +++ b/flatpak/parse.go @@ -1,6 +1,7 @@ package flatpak import ( + "strconv" "strings" "github.com/gogrlx/snack" @@ -127,3 +128,42 @@ func parseRemotes(output string) []snack.Repository { } return repos } + +// semverCmp does a basic semver-ish comparison. +// Returns -1 if a < b, 0 if equal, 1 if a > b. +func semverCmp(a, b string) int { + partsA := strings.Split(a, ".") + partsB := strings.Split(b, ".") + + maxLen := len(partsA) + if len(partsB) > maxLen { + maxLen = len(partsB) + } + + for i := 0; i < maxLen; i++ { + var numA, numB int + if i < len(partsA) { + numA, _ = strconv.Atoi(stripNonNumeric(partsA[i])) + } + if i < len(partsB) { + numB, _ = strconv.Atoi(stripNonNumeric(partsB[i])) + } + if numA < numB { + return -1 + } + if numA > numB { + return 1 + } + } + return 0 +} + +// stripNonNumeric keeps only leading digits from a string. +func stripNonNumeric(s string) string { + for i, c := range s { + if c < '0' || c > '9' { + return s[:i] + } + } + return s +}