A shepherd for your Appimages.
0

Configure Feed

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

normalize appimage names based on .desktop Name

Aly Raffauf (Jun 17, 2026, 7:06 PM EDT) 5a69db91 ec53a1d3

+46 -10
+18 -4
cmd/appherder/desktop_test.go
··· 115 115 } 116 116 117 117 func TestDeriveAppName(t *testing.T) { 118 - if got := deriveAppName("org.kde.krita.desktop", "/dl/krita-5.2.0-x86_64.appimage"); got != "org.kde.krita" { 119 - t.Fatalf("deriveAppName with desktop = %q, want org.kde.krita", got) 118 + tests := []struct { 119 + name string 120 + desktop *desktopFile 121 + desktopName string 122 + appimagePath string 123 + want string 124 + }{ 125 + {"Name with hyphen", parseDesktopFile([]byte("[Desktop Entry]\nName=ES-DE\n")), "org.es_de.frontend.desktop", "", "esde"}, 126 + {"Name with spaces", parseDesktopFile([]byte("[Desktop Entry]\nName=Visual Studio Code\n")), "code.desktop", "", "visual_studio_code"}, 127 + {"Name lowercased", parseDesktopFile([]byte("[Desktop Entry]\nName=Krita\n")), "krita.desktop", "", "krita"}, 128 + {"desktop id fallback", nil, "org.kde.krita.desktop", "", "org.kde.krita"}, 129 + {"filename fallback", nil, "", "/dl/Krita-5.2.0-x86_64.AppImage", "krita5.2.0x86_64"}, 120 130 } 121 - if got := deriveAppName("", "/dl/Krita-5.2.0-x86_64.AppImage"); got != "Krita-5.2.0-x86_64" { 122 - t.Fatalf("deriveAppName fallback = %q, want Krita-5.2.0-x86_64", got) 131 + for _, tc := range tests { 132 + t.Run(tc.name, func(t *testing.T) { 133 + if got := deriveAppName(tc.desktop, tc.desktopName, tc.appimagePath); got != tc.want { 134 + t.Fatalf("deriveAppName = %q, want %q", got, tc.want) 135 + } 136 + }) 123 137 } 124 138 } 125 139
+28 -6
cmd/appherder/install.go
··· 32 32 } 33 33 34 34 icon := resolveIcon(fsys) 35 - appName := deriveAppName(desktopName, appimage) 35 + appName := deriveAppName(desktop, desktopName, appimage) 36 36 37 37 // Patch in memory before any filesystem writes so a failure here installs nothing. 38 38 if desktop != nil { ··· 78 78 return nil 79 79 } 80 80 81 - // deriveAppName prefers the AppImage's desktop-file id, which is stable across 82 - // versions, and falls back to the source filename when no desktop entry ships. 83 - func deriveAppName(desktopName string, appimagePath string) string { 81 + // deriveAppName picks the canonical install name, matching GearLever so the 82 + // two tools land at the same path: the desktop entry's Name field (e.g. 83 + // "ES-DE" -> "esde"), then the desktop-file id, then the source filename. 84 + func deriveAppName(desktop *desktopFile, desktopName string, appimagePath string) string { 85 + if desktop != nil { 86 + if name, ok := desktop.get("Name", desktopEntrySection); ok && name != "" { 87 + return sanitizeAppName(name) 88 + } 89 + } 84 90 if name := strings.TrimSuffix(desktopName, ".desktop"); name != "" { 85 - return name 91 + return sanitizeAppName(name) 86 92 } 87 - return appNameFromPath(appimagePath) 93 + return sanitizeAppName(appNameFromPath(appimagePath)) 94 + } 95 + 96 + // sanitizeAppName lowercases s, turns spaces into underscores, and drops any 97 + // character that isn't alphanumeric, underscore, or dot — GearLever's naming 98 + // rule. 99 + func sanitizeAppName(s string) string { 100 + s = strings.ToLower(s) 101 + s = strings.ReplaceAll(s, " ", "_") 102 + var b strings.Builder 103 + b.Grow(len(s)) 104 + for _, r := range s { 105 + if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '_' || r == '.' { 106 + b.WriteRune(r) 107 + } 108 + } 109 + return b.String() 88 110 } 89 111 90 112 func appNameFromPath(path string) string {