Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/aim-add.1
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
.nh
.TH "AIM-ADD" "1" "Apr 2026" "aim " "Manage AppImages from the terminal without manual desktop setup or update juggling"
.TH "AIM-ADD" "1" "May 2026" "aim " "Manage AppImages from the terminal without manual desktop setup or update juggling"

.SH NAME
aim-add - Install an AppImage from a file, URL, or provider
Expand Down
2 changes: 1 addition & 1 deletion docs/aim-info.1
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
.nh
.TH "AIM-INFO" "1" "Apr 2026" "aim " "Manage AppImages from the terminal without manual desktop setup or update juggling"
.TH "AIM-INFO" "1" "May 2026" "aim " "Manage AppImages from the terminal without manual desktop setup or update juggling"

.SH NAME
aim-info - Show AppImage or package details
Expand Down
2 changes: 1 addition & 1 deletion docs/aim-list.1
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
.nh
.TH "AIM-LIST" "1" "Apr 2026" "aim " "Manage AppImages from the terminal without manual desktop setup or update juggling"
.TH "AIM-LIST" "1" "May 2026" "aim " "Manage AppImages from the terminal without manual desktop setup or update juggling"

.SH NAME
aim-list - List managed AppImages
Expand Down
2 changes: 1 addition & 1 deletion docs/aim-remove.1
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
.nh
.TH "AIM-REMOVE" "1" "Apr 2026" "aim " "Manage AppImages from the terminal without manual desktop setup or update juggling"
.TH "AIM-REMOVE" "1" "May 2026" "aim " "Manage AppImages from the terminal without manual desktop setup or update juggling"

.SH NAME
aim-remove - Remove a managed AppImage or only its desktop integration
Expand Down
2 changes: 1 addition & 1 deletion docs/aim-update.1
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
.nh
.TH "AIM-UPDATE" "1" "Apr 2026" "aim " "Manage AppImages from the terminal without manual desktop setup or update juggling"
.TH "AIM-UPDATE" "1" "May 2026" "aim " "Manage AppImages from the terminal without manual desktop setup or update juggling"

.SH NAME
aim-update - Check, apply, or configure updates
Expand Down
2 changes: 1 addition & 1 deletion docs/aim.1
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
.nh
.TH "AIM" "1" "Apr 2026" "aim dev" "Manage AppImages from the terminal without manual desktop setup or update juggling"
.TH "AIM" "1" "May 2026" "aim dev" "Manage AppImages from the terminal without manual desktop setup or update juggling"

.SH NAME
aim - Manage AppImages from the terminal without manual desktop setup or update juggling
Expand Down
10 changes: 5 additions & 5 deletions internal/core/icon.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ import (
util "github.com/slobbe/appimage-manager/internal/helpers"
)

func InstallDesktopIcon(appID, iconSrc string) (string, string, error) {
appID = strings.TrimSpace(appID)
if appID == "" {
return "", "", fmt.Errorf("app id cannot be empty")
func InstallDesktopIcon(iconID, iconSrc string) (string, string, error) {
iconID = strings.TrimSpace(iconID)
if iconID == "" {
return "", "", fmt.Errorf("icon id cannot be empty")
}

iconSrc = strings.TrimSpace(iconSrc)
Expand All @@ -27,7 +27,7 @@ func InstallDesktopIcon(appID, iconSrc string) (string, string, error) {
}

destDir := iconInstallDir(ext)
destName := appID + ext
destName := iconID + ext

destPath := filepath.Join(destDir, destName)
desktopIconValue := destPath
Expand Down
197 changes: 99 additions & 98 deletions internal/core/integrate.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,84 +54,22 @@ func integrateFromLocalFile(ctx context.Context, src string, confirmUpdateOverwr
appInfo.ID = extractionData.DesktopStem
}

appID := appInfo.ID

outDir := filepath.Join(config.AimDir, appID)
if err := os.MkdirAll(outDir, 0o755); err != nil {
return nil, err
}

tmpDir := (*extractionData).Dir
defer func() {
_ = os.RemoveAll(tmpDir)
}()

extractionData.Dir = outDir

if extractionData.ExecPath, err = util.Move(extractionData.ExecPath, filepath.Join(extractionData.Dir, appID+filepath.Ext(extractionData.ExecPath))); err != nil {
return nil, err
}
if extractionData.DesktopEntryPath, err = util.Move(extractionData.DesktopEntryPath, filepath.Join(extractionData.Dir, appID+filepath.Ext(extractionData.DesktopEntryPath))); err != nil {
return nil, err
}
if extractionData.IconPath, err = util.Move(extractionData.IconPath, filepath.Join(extractionData.Dir, appID+filepath.Ext(extractionData.IconPath))); err != nil {
return nil, err
}

installedIconPath, desktopIconValue, err := InstallDesktopIcon(appID, extractionData.IconPath)
if err != nil {
return nil, err
}
extractionData.IconPath = installedIconPath

if err := UpdateDesktopEntry(ctx, extractionData.DesktopEntryPath, extractionData.ExecPath, desktopIconValue); err != nil {
return nil, err
}

if err := ValidateDesktopEntry(ctx, extractionData.DesktopEntryPath); err != nil {
return nil, err
}

if err := util.MakeExecutable(extractionData.ExecPath); err != nil {
return nil, err
}

desktopEntryLink, err := MakeDesktopLink(extractionData.DesktopEntryPath, appID+".desktop", "aim-"+appID+".desktop")
if err != nil {
return nil, err
}

var existingApp *models.App
var replacementApp *models.App
var updateFromAppImage *models.UpdateSource

var updateInfoWG sync.WaitGroup
updateInfoWG.Add(1)
go func() {
defer updateInfoWG.Done()

updateInfo, err := getEmbeddedUpdateInfo(extractionData.ExecPath)
if err != nil || updateInfo.Kind != models.UpdateZsync {
return
}

if updateInfo, err := getEmbeddedUpdateInfo(extractionData.ExecPath); err == nil && updateInfo.Kind == models.UpdateZsync {
updateFromAppImage = &models.UpdateSource{
Kind: models.UpdateZsync,
Zsync: &models.ZsyncUpdateSource{
UpdateInfo: updateInfo.UpdateInfo,
Transport: updateInfo.Transport,
},
}
}()

if appData, err := repo.GetApp(appID); err == nil {
existingApp = appData
} else if !strings.Contains(err.Error(), "does not exists in database") {
return nil, err
}

updateInfoWG.Wait()

timestampNow := util.NowISO()
addedAt := timestampNow
lastCheckedAt := ""
Expand All @@ -146,6 +84,36 @@ func integrateFromLocalFile(ctx context.Context, src string, confirmUpdateOverwr
update = updateFromAppImage
}

source := models.Source{
Kind: models.SourceLocalFile,
LocalFile: &models.LocalFileSource{
IntegratedAt: timestampNow,
OriginalPath: src,
},
}

upstreamID := appInfo.ID
incomingIdentity := &models.App{
ID: upstreamID,
Name: appInfo.Name,
Source: source,
Update: update,
}

appID, replacementApp, err := ResolveManagedAppID(appInfo.Name, upstreamID, src, incomingIdentity)
if err != nil {
return nil, err
}

var existingApp *models.App
if replacementApp != nil {
existingApp = replacementApp
} else if appData, err := repo.GetApp(appID); err == nil {
existingApp = appData
} else if !strings.Contains(err.Error(), "does not exists in database") {
return nil, err
}

if existingApp != nil {
if strings.TrimSpace(existingApp.AddedAt) != "" {
addedAt = existingApp.AddedAt
Expand All @@ -165,43 +133,48 @@ func integrateFromLocalFile(ctx context.Context, src string, confirmUpdateOverwr
}
}

source := models.Source{
Kind: models.SourceLocalFile,
LocalFile: &models.LocalFileSource{
IntegratedAt: timestampNow,
OriginalPath: src,
},
outDir := filepath.Join(config.AimDir, appID)
if err := os.MkdirAll(outDir, 0o755); err != nil {
return nil, err
}

if existingApp == nil {
match, err := FindEquivalentManagedApp(&models.App{
ID: appID,
Source: source,
Update: update,
})
if err != nil {
return nil, err
}
if match != nil {
existingApp = match
replacementApp = match
if strings.TrimSpace(existingApp.AddedAt) != "" {
addedAt = existingApp.AddedAt
}
lastCheckedAt = existingApp.LastCheckedAt
extractionData.Dir = outDir

if !updateFound {
update = existingApp.Update
} else if existingApp.Update != nil && existingApp.Update.Kind != models.UpdateNone && confirmUpdateOverwrite != nil {
overwrite, err := confirmUpdateOverwrite(existingApp.Update, update)
if err != nil {
return nil, err
}
if !overwrite {
update = existingApp.Update
}
}
}
if extractionData.ExecPath, err = util.Move(extractionData.ExecPath, filepath.Join(extractionData.Dir, appID+filepath.Ext(extractionData.ExecPath))); err != nil {
return nil, err
}
if extractionData.DesktopEntryPath, err = util.Move(extractionData.DesktopEntryPath, filepath.Join(extractionData.Dir, appID+filepath.Ext(extractionData.DesktopEntryPath))); err != nil {
return nil, err
}
if extractionData.IconPath, err = util.Move(extractionData.IconPath, filepath.Join(extractionData.Dir, appID+filepath.Ext(extractionData.IconPath))); err != nil {
return nil, err
}

installedIconPath, desktopIconValue, err := InstallDesktopIcon(appID, extractionData.IconPath)
if err != nil {
return nil, err
}
extractionData.IconPath = installedIconPath

if existingApp != nil && replacementApp == nil {
removeStaleInstalledIcon(existingApp.IconPath, installedIconPath, appID)
}

if err := UpdateDesktopEntry(ctx, extractionData.DesktopEntryPath, extractionData.ExecPath, desktopIconValue); err != nil {
return nil, err
}

if err := ValidateDesktopEntry(ctx, extractionData.DesktopEntryPath); err != nil {
return nil, err
}

if err := util.MakeExecutable(extractionData.ExecPath); err != nil {
return nil, err
}

desktopEntryLink, err := MakeDesktopLink(extractionData.DesktopEntryPath, appID+".desktop", "aim-"+appID+".desktop")
if err != nil {
return nil, err
}

var (
Expand Down Expand Up @@ -236,7 +209,7 @@ func integrateFromLocalFile(ctx context.Context, src string, confirmUpdateOverwr

app := &models.App{
Name: appInfo.Name,
ID: appInfo.ID,
ID: appID,
Version: appInfo.Version,
ExecPath: extractionData.ExecPath,
DesktopEntryPath: extractionData.DesktopEntryPath,
Expand Down Expand Up @@ -326,3 +299,31 @@ func MakeDesktopLink(src, preferredName, fallbackName string) (string, error) {

return desktopLink, nil
}

func removeStaleInstalledIcon(oldPath, newPath, appID string) {
oldPath = filepath.Clean(strings.TrimSpace(oldPath))
newPath = filepath.Clean(strings.TrimSpace(newPath))
if oldPath == "." || oldPath == "" || oldPath == newPath {
return
}

appDir := filepath.Join(config.AimDir, appID)
if oldPath == appDir || strings.HasPrefix(oldPath, appDir+string(filepath.Separator)) {
return
}

allApps, err := repo.GetAllApps()
if err != nil {
return
}
for _, app := range allApps {
if app == nil || strings.TrimSpace(app.ID) == appID {
continue
}
if filepath.Clean(strings.TrimSpace(app.IconPath)) == oldPath {
return
}
}

_ = os.Remove(oldPath)
}
Loading
Loading