A shepherd for your Appimages.
0

Configure Feed

Select the types of activity you want to include in your feed.

centralize dir resolution

Aly Raffauf (Jun 18, 2026, 12:37 AM EDT) 5dc3a50e 615adb08

+123 -145
+6 -1
cmd/appherder/main.go
··· 10 10 var version = "dev" 11 11 12 12 func main() { 13 - cmd := newRootCommand(appherder.NewApp(), os.Stdout, os.Stderr) 13 + app, err := appherder.NewApp() 14 + if err != nil { 15 + fmt.Fprintln(os.Stderr, err) 16 + os.Exit(1) 17 + } 18 + cmd := newRootCommand(app, os.Stdout, os.Stderr) 14 19 cmd.SetArgs(os.Args[1:]) 15 20 if err := cmd.Execute(); err != nil { 16 21 fmt.Fprintln(os.Stderr, err)
+26 -4
internal/appherder/app.go
··· 1 1 package appherder 2 2 3 - import "os" 3 + import ( 4 + "fmt" 5 + "os" 6 + "path/filepath" 7 + ) 4 8 5 9 // App is the core engine for managing AppImages. It holds no CLI or I/O 6 10 // state; all output formatting is the caller's responsibility. 7 11 type App struct { 8 - homeDir func() (string, error) 12 + appimagesDir string 13 + applicationsDir string 14 + iconsDir string 9 15 } 10 16 11 17 // NewApp returns an App wired to the current user's home directory. 12 - func NewApp() App { 18 + func NewApp() (App, error) { 19 + home, err := os.UserHomeDir() 20 + if err != nil { 21 + return App{}, fmt.Errorf("resolve home directory: %w", err) 22 + } 23 + return NewAppWithDirs( 24 + filepath.Join(home, "AppImages"), 25 + filepath.Join(home, ".local", "share", "applications"), 26 + filepath.Join(home, "AppImages", ".icons"), 27 + ), nil 28 + } 29 + 30 + // NewAppWithDirs returns an App that uses the given directories directly, 31 + // for tests or non-standard layouts. 32 + func NewAppWithDirs(appimagesDir, applicationsDir, iconsDir string) App { 13 33 return App{ 14 - homeDir: os.UserHomeDir, 34 + appimagesDir: appimagesDir, 35 + applicationsDir: applicationsDir, 36 + iconsDir: iconsDir, 15 37 } 16 38 }
+4 -10
internal/appherder/appimage.go
··· 97 97 } 98 98 99 99 func (a App) installAppImage(file string, appName string) (string, error) { 100 - home, err := a.homeDir() 101 - if err != nil { 102 - return "", fmt.Errorf("resolve home directory: %w", err) 103 - } 104 - 105 - appimagesDir := filepath.Join(home, "AppImages") 106 - if err := os.MkdirAll(appimagesDir, 0o755); err != nil { 107 - return "", fmt.Errorf("create AppImages directory %s: %w", appimagesDir, err) 100 + if err := os.MkdirAll(a.appimagesDir, 0o755); err != nil { 101 + return "", fmt.Errorf("create AppImages directory %s: %w", a.appimagesDir, err) 108 102 } 109 103 110 - dest := filepath.Join(appimagesDir, appName+".appimage") 111 - inFolder := samePath(filepath.Dir(file), appimagesDir) 104 + dest := filepath.Join(a.appimagesDir, appName+".appimage") 105 + inFolder := samePath(filepath.Dir(file), a.appimagesDir) 112 106 113 107 if !samePath(file, dest) { 114 108 same, err := sameContent(file, dest)
+5 -10
internal/appherder/appimage_test.go
··· 58 58 } 59 59 60 60 func TestInstallAppImageCopiesFromElsewhere(t *testing.T) { 61 - home := t.TempDir() 61 + a, home := newTestApp(t) 62 62 src := filepath.Join(t.TempDir(), "Foo-1.0-x86_64.AppImage") 63 63 if err := os.WriteFile(src, []byte("payload"), 0o644); err != nil { 64 64 t.Fatal(err) 65 65 } 66 - a := App{homeDir: func() (string, error) { return home, nil }} 67 66 68 67 dest, err := a.installAppImage(src, "foo") 69 68 if err != nil { ··· 79 78 } 80 79 81 80 func TestInstallAppImageMovesVersionedFileInPlace(t *testing.T) { 82 - home := t.TempDir() 81 + a, home := newTestApp(t) 83 82 appimages := filepath.Join(home, "AppImages") 84 83 if err := os.MkdirAll(appimages, 0o755); err != nil { 85 84 t.Fatal(err) ··· 88 87 if err := os.WriteFile(src, []byte("payload"), 0o644); err != nil { 89 88 t.Fatal(err) 90 89 } 91 - a := App{homeDir: func() (string, error) { return home, nil }} 92 90 93 91 dest, err := a.installAppImage(src, "foo") 94 92 if err != nil { ··· 101 99 } 102 100 103 101 func TestInstallAppImageSkipsCopyWhenIdentical(t *testing.T) { 104 - home := t.TempDir() 102 + a, home := newTestApp(t) 105 103 appimages := filepath.Join(home, "AppImages") 106 104 if err := os.MkdirAll(appimages, 0o755); err != nil { 107 105 t.Fatal(err) ··· 119 117 if err != nil { 120 118 t.Fatal(err) 121 119 } 122 - a := App{homeDir: func() (string, error) { return home, nil }} 123 120 if _, err := a.installAppImage(src, "foo"); err != nil { 124 121 t.Fatal(err) 125 122 } ··· 137 134 } 138 135 139 136 func TestInstallAppImageRemovesIdenticalDuplicateInFolder(t *testing.T) { 140 - home := t.TempDir() 137 + a, home := newTestApp(t) 141 138 appimages := filepath.Join(home, "AppImages") 142 139 if err := os.MkdirAll(appimages, 0o755); err != nil { 143 140 t.Fatal(err) ··· 155 152 if err != nil { 156 153 t.Fatal(err) 157 154 } 158 - a := App{homeDir: func() (string, error) { return home, nil }} 159 155 if _, err := a.installAppImage(dup, "foo"); err != nil { 160 156 t.Fatal(err) 161 157 } ··· 173 169 } 174 170 175 171 func TestInstallAppImageNoOpWhenAlreadyCanonical(t *testing.T) { 176 - home := t.TempDir() 172 + a, home := newTestApp(t) 177 173 appimages := filepath.Join(home, "AppImages") 178 174 if err := os.MkdirAll(appimages, 0o755); err != nil { 179 175 t.Fatal(err) ··· 182 178 if err := os.WriteFile(dest, []byte("payload"), 0o644); err != nil { 183 179 t.Fatal(err) 184 180 } 185 - a := App{homeDir: func() (string, error) { return home, nil }} 186 181 187 182 got, err := a.installAppImage(dest, "foo") 188 183 if err != nil {
+5 -16
internal/appherder/desktop.go
··· 137 137 138 138 // managedApps returns the ids of desktop entries appherder installed, found by 139 139 // scanning the user applications directory for the ownership marker. 140 - func managedApps(home string) ([]string, error) { 141 - matches, err := filepath.Glob(filepath.Join(home, ".local", "share", "applications", "*.desktop")) 140 + func managedApps(applicationsDir string) ([]string, error) { 141 + matches, err := filepath.Glob(filepath.Join(applicationsDir, "*.desktop")) 142 142 if err != nil { 143 143 return nil, err 144 144 } ··· 240 240 } 241 241 242 242 func (a App) patchDesktopFile(desktop *desktopFile, appName string, hasIcon bool) error { 243 - home, err := a.homeDir() 244 - if err != nil { 245 - return fmt.Errorf("resolve home directory: %w", err) 246 - } 247 - 248 - appimages := filepath.Join(home, "AppImages") 249 - appimage := filepath.Join(appimages, appName+".appimage") 243 + appimage := filepath.Join(a.appimagesDir, appName+".appimage") 250 244 251 245 desktop.set(desktopOwnerKey, "true", desktopEntrySection) 252 246 if hasIcon { 253 - desktop.set("Icon", filepath.Join(appimages, ".icons", appName), desktopEntrySection) 247 + desktop.set("Icon", filepath.Join(a.iconsDir, appName), desktopEntrySection) 254 248 } 255 249 desktop.set("TryExec", appimage, desktopEntrySection) 256 250 ··· 273 267 } 274 268 275 269 func (a App) installDesktopFile(desktop *desktopFile, appName string) (string, error) { 276 - home, err := a.homeDir() 277 - if err != nil { 278 - return "", fmt.Errorf("resolve home directory: %w", err) 279 - } 280 - 281 - dest := filepath.Join(home, ".local", "share", "applications", appName+".desktop") 270 + dest := filepath.Join(a.applicationsDir, appName+".desktop") 282 271 if err := os.MkdirAll(filepath.Dir(dest), 0o755); err != nil { 283 272 return "", fmt.Errorf("create desktop file directory %s: %w", filepath.Dir(dest), err) 284 273 }
+11 -11
internal/appherder/desktop_test.go
··· 49 49 } 50 50 51 51 func TestPatchDesktopFilePreservesDesktopActions(t *testing.T) { 52 + a, home := newTestApp(t) 53 + 52 54 dir := t.TempDir() 53 55 source := filepath.Join(dir, "source.desktop") 54 - output := filepath.Join(dir, "output.desktop") 55 56 if err := os.WriteFile(source, []byte(sampleDesktopFile), 0o644); err != nil { 56 57 t.Fatal(err) 57 58 } 58 59 59 - a := App{homeDir: func() (string, error) { return "/home/test", nil }} 60 - 61 60 desktop, err := readDesktopFile(source) 62 61 if err != nil { 63 62 t.Fatal(err) ··· 65 64 if err := a.patchDesktopFile(desktop, "example", true); err != nil { 66 65 t.Fatal(err) 67 66 } 67 + output := filepath.Join(dir, "output.desktop") 68 68 if err := desktop.write(output); err != nil { 69 69 t.Fatal(err) 70 70 } ··· 75 75 } 76 76 77 77 assertDesktopValue(t, patched, desktopEntrySection, desktopOwnerKey, "true") 78 - assertDesktopValue(t, patched, desktopEntrySection, "Icon", "/home/test/AppImages/.icons/example") 79 - assertDesktopValue(t, patched, desktopEntrySection, "TryExec", "/home/test/AppImages/example.appimage") 80 - assertDesktopExec(t, patched, desktopEntrySection, []string{"env", "FOO=bar", "DESKTOPINTEGRATION=1", "/home/test/AppImages/example.appimage", "%U"}) 81 - assertDesktopExec(t, patched, "Desktop Action new-window", []string{"env", "DESKTOPINTEGRATION=1", "/home/test/AppImages/example.appimage", "--new-window", "%U"}) 82 - assertDesktopExec(t, patched, "Desktop Action new-private-window", []string{"env", "DESKTOPINTEGRATION=1", "/home/test/AppImages/example.appimage", "--private-window", "%U"}) 78 + assertDesktopValue(t, patched, desktopEntrySection, "Icon", filepath.Join(home, "AppImages", ".icons", "example")) 79 + assertDesktopValue(t, patched, desktopEntrySection, "TryExec", filepath.Join(home, "AppImages", "example.appimage")) 80 + assertDesktopExec(t, patched, desktopEntrySection, []string{"env", "FOO=bar", "DESKTOPINTEGRATION=1", filepath.Join(home, "AppImages", "example.appimage"), "%U"}) 81 + assertDesktopExec(t, patched, "Desktop Action new-window", []string{"env", "DESKTOPINTEGRATION=1", filepath.Join(home, "AppImages", "example.appimage"), "--new-window", "%U"}) 82 + assertDesktopExec(t, patched, "Desktop Action new-private-window", []string{"env", "DESKTOPINTEGRATION=1", filepath.Join(home, "AppImages", "example.appimage"), "--private-window", "%U"}) 83 83 assertDesktopValue(t, patched, "Desktop Action new-private-window", "Name", "New Private Window") 84 84 } 85 85 ··· 138 138 } 139 139 140 140 func TestPatchDesktopFileSetsExecWhenMissing(t *testing.T) { 141 - a := App{homeDir: func() (string, error) { return "/home/test", nil }} 141 + a, home := newTestApp(t) 142 142 desktop := parseDesktopFile([]byte( 143 143 "[Desktop Entry]\nType=Application\nName=Foo\nTerminal=true\n", 144 144 )) ··· 146 146 t.Fatal(err) 147 147 } 148 148 assertDesktopValue(t, desktop, desktopEntrySection, "Terminal", "true") 149 - assertDesktopValue(t, desktop, desktopEntrySection, "TryExec", "/home/test/AppImages/foo.appimage") 150 - assertDesktopValue(t, desktop, desktopEntrySection, "Exec", "/home/test/AppImages/foo.appimage") 149 + assertDesktopValue(t, desktop, desktopEntrySection, "TryExec", filepath.Join(home, "AppImages", "foo.appimage")) 150 + assertDesktopValue(t, desktop, desktopEntrySection, "Exec", filepath.Join(home, "AppImages", "foo.appimage")) 151 151 assertDesktopValue(t, desktop, desktopEntrySection, desktopOwnerKey, "true") 152 152 } 153 153
+30
internal/appherder/helpers_test.go
··· 1 + package appherder 2 + 3 + import ( 4 + "os" 5 + "path/filepath" 6 + "testing" 7 + ) 8 + 9 + // newTestApp returns an App whose directories live under a temp dir. 10 + func newTestApp(t *testing.T) (App, string) { 11 + t.Helper() 12 + home := t.TempDir() 13 + return NewAppWithDirs( 14 + filepath.Join(home, "AppImages"), 15 + filepath.Join(home, ".local", "share", "applications"), 16 + filepath.Join(home, "AppImages", ".icons"), 17 + ), home 18 + } 19 + 20 + // writeManagedDesktop stamps a launcher with appherder's ownership marker. 21 + func writeManagedDesktop(t *testing.T, home, appid string) { 22 + t.Helper() 23 + dir := filepath.Join(home, ".local", "share", "applications") 24 + if err := os.MkdirAll(dir, 0o755); err != nil { 25 + t.Fatal(err) 26 + } 27 + if err := os.WriteFile(filepath.Join(dir, appid+".desktop"), []byte(managedDesktop), 0o644); err != nil { 28 + t.Fatal(err) 29 + } 30 + }
+3 -9
internal/appherder/icons.go
··· 86 86 } 87 87 88 88 func (a App) installIcon(fsys fs.FS, icon string, appName string) (string, error) { 89 - home, err := a.homeDir() 90 - if err != nil { 91 - return "", fmt.Errorf("resolve home directory: %w", err) 92 - } 93 - 94 - iconsDir := filepath.Join(home, "AppImages", ".icons") 95 - if err := os.MkdirAll(iconsDir, 0o755); err != nil { 96 - return "", fmt.Errorf("create icon directory %s: %w", iconsDir, err) 89 + if err := os.MkdirAll(a.iconsDir, 0o755); err != nil { 90 + return "", fmt.Errorf("create icon directory %s: %w", a.iconsDir, err) 97 91 } 98 92 99 - dest := filepath.Join(iconsDir, appName) 93 + dest := filepath.Join(a.iconsDir, appName) 100 94 if err := copyFromFS(fsys, icon, dest); err != nil { 101 95 return "", fmt.Errorf("install icon %s to %s: %w", icon, dest, err) 102 96 }
+2 -4
internal/appherder/install_url_test.go
··· 15 15 })) 16 16 defer srv.Close() 17 17 18 - home := t.TempDir() 19 - a := App{homeDir: func() (string, error) { return home, nil }} 18 + a, _ := newTestApp(t) 20 19 _, err := a.InstallFromURL(context.Background(), srv.URL+"/Foo.AppImage") 21 20 if err == nil { 22 21 t.Fatal("expected error for non-AppImage content") ··· 34 33 })) 35 34 defer srv.Close() 36 35 37 - home := t.TempDir() 38 - a := App{homeDir: func() (string, error) { return home, nil }} 36 + a, _ := newTestApp(t) 39 37 if _, err := a.InstallFromURL(context.Background(), srv.URL+"/Foo.AppImage"); err == nil { 40 38 t.Fatal("expected error for 404 download") 41 39 }
+2 -11
internal/appherder/list.go
··· 1 1 package appherder 2 2 3 3 import ( 4 - "fmt" 5 4 "os" 6 5 "path/filepath" 7 6 "sort" ··· 20 19 // List returns display metadata for every app appherder manages. Offline and 21 20 // instant; use CheckUpgrades to see what's stale. 22 21 func (a App) List() ([]AppInfo, error) { 23 - home, err := a.homeDir() 24 - if err != nil { 25 - return nil, fmt.Errorf("resolve home directory: %w", err) 26 - } 27 - 28 - appids, err := managedApps(home) 22 + appids, err := managedApps(a.applicationsDir) 29 23 if err != nil { 30 24 return nil, err 31 25 } 32 26 33 - appimagesDir := filepath.Join(home, "AppImages") 34 - appsDir := filepath.Join(home, ".local", "share", "applications") 35 - 36 27 infos := make([]AppInfo, 0, len(appids)) 37 28 for _, appid := range appids { 38 - infos = append(infos, gatherAppInfo(appsDir, appimagesDir, appid)) 29 + infos = append(infos, gatherAppInfo(a.applicationsDir, a.appimagesDir, appid)) 39 30 } 40 31 sort.Slice(infos, func(i, j int) bool { return infos[i].Name < infos[j].Name }) 41 32 return infos, nil
+3 -6
internal/appherder/list_test.go
··· 7 7 ) 8 8 9 9 func TestListShowsInstalledAndOrphaned(t *testing.T) { 10 - home := t.TempDir() 10 + a, home := newTestApp(t) 11 11 appimages := filepath.Join(home, "AppImages") 12 12 appsDir := filepath.Join(home, ".local", "share", "applications") 13 13 if err := os.MkdirAll(appsDir, 0o755); err != nil { ··· 38 38 t.Fatal(err) 39 39 } 40 40 41 - a := App{homeDir: func() (string, error) { return home, nil }} 42 41 infos, err := a.List() 43 42 if err != nil { 44 43 t.Fatal(err) ··· 72 71 } 73 72 74 73 func TestListEmptyWhenNothingManaged(t *testing.T) { 75 - home := t.TempDir() 76 - a := App{homeDir: func() (string, error) { return home, nil }} 74 + a, _ := newTestApp(t) 77 75 infos, err := a.List() 78 76 if err != nil { 79 77 t.Fatal(err) ··· 84 82 } 85 83 86 84 func TestListFallsBackToFilenameForName(t *testing.T) { 87 - home := t.TempDir() 85 + a, home := newTestApp(t) 88 86 appimages := filepath.Join(home, "AppImages") 89 87 appsDir := filepath.Join(home, ".local", "share", "applications") 90 88 if err := os.MkdirAll(appimages, 0o755); err != nil { ··· 104 102 t.Fatal(err) 105 103 } 106 104 107 - a := App{homeDir: func() (string, error) { return home, nil }} 108 105 infos, err := a.List() 109 106 if err != nil { 110 107 t.Fatal(err)
+6 -13
internal/appherder/sync.go
··· 40 40 // (entries left by another tool). Per-file errors are included in the result 41 41 // rather than aborting the pass. 42 42 func (a App) Sync(ctx context.Context, force bool) (SyncResult, error) { 43 - home, err := a.homeDir() 44 - if err != nil { 45 - return SyncResult{}, fmt.Errorf("resolve home directory: %w", err) 46 - } 47 - appimagesDir := filepath.Join(home, "AppImages") 48 - 49 - existing, err := managedApps(home) 43 + existing, err := managedApps(a.applicationsDir) 50 44 if err != nil { 51 45 return SyncResult{}, err 52 46 } ··· 55 49 managed[appid] = true 56 50 } 57 51 58 - files, err := listAppImages(appimagesDir) 52 + files, err := listAppImages(a.appimagesDir) 59 53 if err != nil { 60 54 return SyncResult{}, err 61 55 } ··· 76 70 77 71 candidates := existing 78 72 if force { 79 - extra, err := appImageBackedOrphans(home, appimagesDir) 73 + extra, err := appImageBackedOrphans(a.applicationsDir, a.appimagesDir) 80 74 if err != nil { 81 75 return result, err 82 76 } 83 77 candidates = append(candidates, extra...) 84 78 } 85 79 for _, appid := range candidates { 86 - present, err := appImagePresent(appimagesDir, appid) 80 + present, err := appImagePresent(a.appimagesDir, appid) 87 81 if err != nil { 88 82 return result, err 89 83 } ··· 157 151 // appImageBackedOrphans returns appids of unmanaged desktop entries whose 158 152 // TryExec or Exec points at a missing file inside appimagesDir — launchers left 159 153 // by another tool after their AppImage was deleted. 160 - func appImageBackedOrphans(home, appimagesDir string) ([]string, error) { 161 - appsDir := filepath.Join(home, ".local", "share", "applications") 162 - matches, err := filepath.Glob(filepath.Join(appsDir, "*.desktop")) 154 + func appImageBackedOrphans(applicationsDir, appimagesDir string) ([]string, error) { 155 + matches, err := filepath.Glob(filepath.Join(applicationsDir, "*.desktop")) 163 156 if err != nil { 164 157 return nil, err 165 158 }
+8 -28
internal/appherder/sync_test.go
··· 8 8 "testing" 9 9 ) 10 10 11 - // writeManagedDesktop stamps a launcher with appherder's ownership marker. 12 - func writeManagedDesktop(t *testing.T, home, appid string) { 13 - t.Helper() 14 - dir := filepath.Join(home, ".local", "share", "applications") 15 - if err := os.MkdirAll(dir, 0o755); err != nil { 16 - t.Fatal(err) 17 - } 18 - if err := os.WriteFile(filepath.Join(dir, appid+".desktop"), []byte(managedDesktop), 0o644); err != nil { 19 - t.Fatal(err) 20 - } 21 - } 22 - 23 11 func TestSyncRemovesOrphanedManagedLauncher(t *testing.T) { 24 - home := t.TempDir() 12 + a, home := newTestApp(t) 25 13 if err := os.MkdirAll(filepath.Join(home, "AppImages"), 0o755); err != nil { 26 14 t.Fatal(err) 27 15 } 28 16 writeManagedDesktop(t, home, "gone") 29 17 30 - a := App{homeDir: func() (string, error) { return home, nil }} 31 18 result, err := a.Sync(context.Background(), false) 32 19 if err != nil { 33 20 t.Fatal(err) ··· 42 29 } 43 30 44 31 func TestSyncKeepsManagedLauncherWhenAppImagePresent(t *testing.T) { 45 - home := t.TempDir() 32 + a, home := newTestApp(t) 46 33 appimages := filepath.Join(home, "AppImages") 47 34 if err := os.MkdirAll(appimages, 0o755); err != nil { 48 35 t.Fatal(err) ··· 54 41 } 55 42 writeManagedDesktop(t, home, "present") 56 43 57 - a := App{homeDir: func() (string, error) { return home, nil }} 58 44 result, err := a.Sync(context.Background(), false) 59 45 if err != nil { 60 46 t.Fatal(err) ··· 69 55 } 70 56 71 57 func TestSyncKeepsUnmanagedLauncherEvenWhenAppImageAbsent(t *testing.T) { 72 - home := t.TempDir() 58 + a, home := newTestApp(t) 73 59 if err := os.MkdirAll(filepath.Join(home, "AppImages"), 0o755); err != nil { 74 60 t.Fatal(err) 75 61 } ··· 83 69 t.Fatal(err) 84 70 } 85 71 86 - a := App{homeDir: func() (string, error) { return home, nil }} 87 72 for _, force := range []bool{false, true} { 88 73 if _, err := a.Sync(context.Background(), force); err != nil { 89 74 t.Fatal(err) ··· 95 80 } 96 81 97 82 func TestSyncForceRemovesAppImageBackedOrphan(t *testing.T) { 98 - home := t.TempDir() 83 + a, home := newTestApp(t) 99 84 appimages := filepath.Join(home, "AppImages") 100 85 if err := os.MkdirAll(appimages, 0o755); err != nil { 101 86 t.Fatal(err) ··· 111 96 t.Fatal(err) 112 97 } 113 98 114 - a := App{homeDir: func() (string, error) { return home, nil }} 115 99 result, err := a.Sync(context.Background(), true) 116 100 if err != nil { 117 101 t.Fatal(err) ··· 125 109 } 126 110 127 111 func TestSyncForceKeepsOrphanWhenAppImageStillPresent(t *testing.T) { 128 - home := t.TempDir() 112 + a, home := newTestApp(t) 129 113 appimages := filepath.Join(home, "AppImages") 130 114 if err := os.MkdirAll(appimages, 0o755); err != nil { 131 115 t.Fatal(err) ··· 144 128 t.Fatal(err) 145 129 } 146 130 147 - a := App{homeDir: func() (string, error) { return home, nil }} 148 131 if _, err := a.Sync(context.Background(), true); err != nil { 149 132 t.Fatal(err) 150 133 } ··· 154 137 } 155 138 156 139 func TestSyncIgnoresHiddenAndTempFiles(t *testing.T) { 157 - home := t.TempDir() 140 + a, home := newTestApp(t) 158 141 appimages := filepath.Join(home, "AppImages") 159 142 if err := os.MkdirAll(appimages, 0o755); err != nil { 160 143 t.Fatal(err) ··· 166 149 } 167 150 } 168 151 169 - a := App{homeDir: func() (string, error) { return home, nil }} 170 152 result, err := a.Sync(context.Background(), false) 171 153 if err != nil { 172 154 t.Fatal(err) ··· 177 159 } 178 160 179 161 func TestSyncHandlesMissingAppImagesDir(t *testing.T) { 180 - home := t.TempDir() 162 + a, home := newTestApp(t) 181 163 writeManagedDesktop(t, home, "orphan") 182 164 183 - a := App{homeDir: func() (string, error) { return home, nil }} 184 165 if _, err := a.Sync(context.Background(), false); err != nil { 185 166 t.Fatal(err) 186 167 } ··· 190 171 } 191 172 192 173 func TestSyncReportsSkipsInInputOrder(t *testing.T) { 193 - home := t.TempDir() 174 + a, home := newTestApp(t) 194 175 appimages := filepath.Join(home, "AppImages") 195 176 if err := os.MkdirAll(appimages, 0o755); err != nil { 196 177 t.Fatal(err) ··· 202 183 } 203 184 } 204 185 205 - a := App{homeDir: func() (string, error) { return home, nil }} 206 186 result, err := a.Sync(context.Background(), false) 207 187 if err != nil { 208 188 t.Fatal(err)
+5 -9
internal/appherder/uninstall.go
··· 10 10 11 11 func (a App) Uninstall(name string, force bool) error { 12 12 appName := NormalizeAppName(name) 13 - home, err := a.homeDir() 14 - if err != nil { 15 - return fmt.Errorf("resolve home directory: %w", err) 16 - } 17 13 18 - for _, path := range installedPaths(home, appName) { 14 + for _, path := range a.installedPaths(appName) { 19 15 if strings.HasSuffix(path, ".desktop") && !force { 20 16 managed, err := isManagedDesktop(path) 21 17 if err != nil { ··· 44 40 return name 45 41 } 46 42 47 - func installedPaths(home string, appName string) []string { 43 + func (a App) installedPaths(appName string) []string { 48 44 return []string{ 49 - filepath.Join(home, "AppImages", appName+".appimage"), 50 - filepath.Join(home, "AppImages", ".icons", appName), 51 - filepath.Join(home, ".local", "share", "applications", appName+".desktop"), 45 + filepath.Join(a.appimagesDir, appName+".appimage"), 46 + filepath.Join(a.iconsDir, appName), 47 + filepath.Join(a.applicationsDir, appName+".desktop"), 52 48 } 53 49 }
+6 -8
internal/appherder/uninstall_test.go
··· 29 29 } 30 30 31 31 func TestUninstallRemovesInstalledFilesOnly(t *testing.T) { 32 - home := t.TempDir() 33 - for _, path := range installedPaths(home, "example") { 32 + a, home := newTestApp(t) 33 + for _, path := range a.installedPaths("example") { 34 34 if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { 35 35 t.Fatal(err) 36 36 } ··· 43 43 } 44 44 } 45 45 46 - a := App{homeDir: func() (string, error) { return home, nil }} 47 46 if err := a.Uninstall(filepath.Join(home, "AppImages", "example.AppImage"), false); err != nil { 48 47 t.Fatal(err) 49 48 } 50 49 51 - for _, path := range installedPaths(home, "example") { 50 + for _, path := range a.installedPaths("example") { 52 51 if _, err := os.Stat(path); !errors.Is(err, os.ErrNotExist) { 53 52 t.Fatalf("expected %s to be removed, stat err: %v", path, err) 54 53 } ··· 56 55 } 57 56 58 57 func TestUninstallKeepsUnmanagedDesktopFile(t *testing.T) { 59 - home := t.TempDir() 58 + a, home := newTestApp(t) 60 59 desktop := filepath.Join(home, ".local", "share", "applications", "example.desktop") 61 60 if err := os.MkdirAll(filepath.Dir(desktop), 0o755); err != nil { 62 61 t.Fatal(err) ··· 66 65 t.Fatal(err) 67 66 } 68 67 69 - a := App{homeDir: func() (string, error) { return home, nil }} 70 68 if err := a.Uninstall("example", false); err != nil { 71 69 t.Fatal(err) 72 70 } ··· 84 82 } 85 83 86 84 func TestManagedApps(t *testing.T) { 87 - home := t.TempDir() 85 + _, home := newTestApp(t) 88 86 dir := filepath.Join(home, ".local", "share", "applications") 89 87 if err := os.MkdirAll(dir, 0o755); err != nil { 90 88 t.Fatal(err) ··· 96 94 t.Fatal(err) 97 95 } 98 96 99 - got, err := managedApps(home) 97 + got, err := managedApps(dir) 100 98 if err != nil { 101 99 t.Fatal(err) 102 100 }
+1 -5
internal/appherder/upgrade.go
··· 36 36 // already current are included with NoSource/Available=false so the caller 37 37 // can decide what to show. 38 38 func (a App) CheckUpgrades(ctx context.Context) ([]UpgradeCheck, error) { 39 - home, err := a.homeDir() 40 - if err != nil { 41 - return nil, fmt.Errorf("resolve home directory: %w", err) 42 - } 43 - files, err := listAppImages(filepath.Join(home, "AppImages")) 39 + files, err := listAppImages(a.appimagesDir) 44 40 if err != nil { 45 41 return nil, err 46 42 }