Files
snack/snap/parse.go
Tai Groot 4beeb0a0d7 feat: add flatpak and snap package manager implementations
flatpak: implements Manager, Cleaner, and RepoManager interfaces
- Install/Remove/Purge/Upgrade with flatpak CLI
- Repository management (add/remove/list remotes)
- Autoremove unused runtimes

snap: implements Manager and VersionQuerier interfaces
- Install with --classic/--channel support via Target.FromRepo
- Remove/Purge/Upgrade via snap CLI
- Version queries with semver comparison
- Upgrade availability via snap refresh --list

Both packages follow the existing pattern:
- Exported methods on struct delegate to unexported functions
- _linux.go for real implementation, _other.go stubs
- Compile-time interface checks
- Parser tests for all output formats
2026-02-25 22:23:26 +00:00

184 lines
4.0 KiB
Go

package snap
import (
"strconv"
"strings"
"github.com/gogrlx/snack"
)
// parseSnapList parses `snap list` tabular output.
// Header: Name Version Rev Tracking Publisher Notes
func parseSnapList(output string) []snack.Package {
var pkgs []snack.Package
lines := strings.Split(output, "\n")
for i, line := range lines {
if i == 0 { // skip header
continue
}
line = strings.TrimSpace(line)
if line == "" {
continue
}
fields := strings.Fields(line)
if len(fields) < 2 {
continue
}
pkg := snack.Package{
Name: fields[0],
Version: fields[1],
Installed: true,
}
pkgs = append(pkgs, pkg)
}
return pkgs
}
// parseSnapFind parses `snap find <query>` tabular output.
// Header: Name Version Publisher Notes Summary
func parseSnapFind(output string) []snack.Package {
var pkgs []snack.Package
lines := strings.Split(output, "\n")
for i, line := range lines {
if i == 0 { // skip header
continue
}
line = strings.TrimSpace(line)
if line == "" {
continue
}
fields := strings.Fields(line)
if len(fields) < 4 {
continue
}
pkg := snack.Package{
Name: fields[0],
Version: fields[1],
}
// Summary is everything after the 4th field
if len(fields) > 4 {
pkg.Description = strings.Join(fields[4:], " ")
}
pkgs = append(pkgs, pkg)
}
return pkgs
}
// parseSnapInfo parses `snap info <pkg>` key:value output.
func parseSnapInfo(output string) *snack.Package {
pkg := &snack.Package{}
for _, line := range strings.Split(output, "\n") {
idx := strings.Index(line, ":")
if idx < 0 {
continue
}
key := strings.TrimSpace(line[:idx])
val := strings.TrimSpace(line[idx+1:])
switch key {
case "name":
pkg.Name = val
case "summary":
pkg.Description = val
case "installed":
// "installed: 1.2.3 (rev) 100MB ..."
parts := strings.Fields(val)
if len(parts) >= 1 {
pkg.Version = parts[0]
pkg.Installed = true
}
case "snap-id":
// presence indicates it exists
}
}
if pkg.Name == "" {
return nil
}
return pkg
}
// parseSnapInfoVersion extracts the latest/stable version from `snap info` output.
func parseSnapInfoVersion(output string) string {
// Look for "latest/stable:" line
for _, line := range strings.Split(output, "\n") {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "latest/stable:") {
val := strings.TrimPrefix(line, "latest/stable:")
val = strings.TrimSpace(val)
fields := strings.Fields(val)
if len(fields) >= 1 && fields[0] != "--" && fields[0] != "^" {
return fields[0]
}
}
}
return ""
}
// parseSnapRefreshList parses `snap refresh --list` tabular output.
// Header: Name Version Rev Publisher Notes
func parseSnapRefreshList(output string) []snack.Package {
var pkgs []snack.Package
lines := strings.Split(output, "\n")
for i, line := range lines {
if i == 0 { // skip header
continue
}
line = strings.TrimSpace(line)
if line == "" {
continue
}
// "All snaps up to date." means no upgrades
if strings.Contains(line, "All snaps up to date") {
return nil
}
fields := strings.Fields(line)
if len(fields) < 2 {
continue
}
pkgs = append(pkgs, snack.Package{
Name: fields[0],
Version: fields[1],
Installed: true,
})
}
return pkgs
}
// 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
}