A shepherd for your Appimages.
0

Configure Feed

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

support uninstalls

Aly Raffauf (Jun 17, 2026, 2:50 PM EDT) c4bc9003 69d6a756

+114 -1
+12 -1
src/cli.go
··· 15 15 } 16 16 cmd.SetOut(stdout) 17 17 cmd.SetErr(stderr) 18 - cmd.AddCommand(newInstallCommand(a)) 18 + cmd.AddCommand(newInstallCommand(a), newUninstallCommand(a)) 19 19 return cmd 20 20 } 21 21 ··· 29 29 }, 30 30 } 31 31 } 32 + 33 + func newUninstallCommand(a app) *cobra.Command { 34 + return &cobra.Command{ 35 + Use: "uninstall APP|APPIMAGE", 36 + Short: "Uninstall an AppImage", 37 + Args: cobra.ExactArgs(1), 38 + RunE: func(cmd *cobra.Command, args []string) error { 39 + return a.uninstall(args[0]) 40 + }, 41 + } 42 + }
+41
src/uninstall.go
··· 1 + package main 2 + 3 + import ( 4 + "errors" 5 + "fmt" 6 + "os" 7 + "path/filepath" 8 + "strings" 9 + ) 10 + 11 + func (a app) uninstall(name string) error { 12 + appName := normalizeAppName(name) 13 + home, err := a.homeDir() 14 + if err != nil { 15 + return fmt.Errorf("resolve home directory: %w", err) 16 + } 17 + 18 + for _, path := range installedPaths(home, appName) { 19 + if err := os.Remove(path); err != nil && !errors.Is(err, os.ErrNotExist) { 20 + return fmt.Errorf("remove %s: %w", path, err) 21 + } 22 + } 23 + 24 + return nil 25 + } 26 + 27 + func normalizeAppName(name string) string { 28 + name = filepath.Base(name) 29 + if ext := filepath.Ext(name); strings.EqualFold(ext, ".appimage") { 30 + name = strings.TrimSuffix(name, ext) 31 + } 32 + return name 33 + } 34 + 35 + func installedPaths(home string, appName string) []string { 36 + return []string{ 37 + filepath.Join(home, "AppImages", appName+".appimage"), 38 + filepath.Join(home, "AppImages", ".icons", appName), 39 + filepath.Join(home, ".local", "share", "applications", appName+".desktop"), 40 + } 41 + }
+61
src/uninstall_test.go
··· 1 + package main 2 + 3 + import ( 4 + "errors" 5 + "os" 6 + "path/filepath" 7 + "reflect" 8 + "testing" 9 + ) 10 + 11 + func TestNormalizeAppNameAcceptsNamesAndAppImagePaths(t *testing.T) { 12 + tests := map[string]string{ 13 + "example": "example", 14 + "example.appimage": "example", 15 + "example.AppImage": "example", 16 + "example.APPIMAGE": "example", 17 + "/home/test/AppImages/example.AppImage": "example", 18 + "/home/test/AppImages/example.appimage": "example", 19 + } 20 + 21 + for input, want := range tests { 22 + if got := normalizeAppName(input); got != want { 23 + t.Fatalf("normalizeAppName(%q) = %q, want %q", input, got, want) 24 + } 25 + } 26 + } 27 + 28 + func TestInstalledPaths(t *testing.T) { 29 + got := installedPaths("/home/test", "example") 30 + want := []string{ 31 + "/home/test/AppImages/example.appimage", 32 + "/home/test/AppImages/.icons/example", 33 + "/home/test/.local/share/applications/example.desktop", 34 + } 35 + if !reflect.DeepEqual(got, want) { 36 + t.Fatalf("installedPaths() = %#v, want %#v", got, want) 37 + } 38 + } 39 + 40 + func TestUninstallRemovesInstalledFilesOnly(t *testing.T) { 41 + home := t.TempDir() 42 + for _, path := range installedPaths(home, "example") { 43 + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { 44 + t.Fatal(err) 45 + } 46 + if err := os.WriteFile(path, []byte("owned"), 0o644); err != nil { 47 + t.Fatal(err) 48 + } 49 + } 50 + 51 + a := app{homeDir: func() (string, error) { return home, nil }} 52 + if err := a.uninstall(filepath.Join(home, "AppImages", "example.AppImage")); err != nil { 53 + t.Fatal(err) 54 + } 55 + 56 + for _, path := range installedPaths(home, "example") { 57 + if _, err := os.Stat(path); !errors.Is(err, os.ErrNotExist) { 58 + t.Fatalf("expected %s to be removed, stat err: %v", path, err) 59 + } 60 + } 61 + }