A shepherd for your Appimages.
0

Configure Feed

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

add source overrides to config

Aly Raffauf (Jun 19, 2026, 3:09 AM EDT) 6015b972 53d53e14

+158 -19
+4
README.md
··· 131 131 132 132 `appherder list` shows each app's status in the **SIGNATURE** column: `pinned` (key locked in), `signed` (carries a signature appherder hasn't pinned yet), or `none`. 133 133 134 + ## Configuration 135 + 136 + AppHerder's directories and update sources can be customized via `~/.config/appherder/config.toml`. See [Configuration](docs/Configuration.md) for all options. 137 + 134 138 ## Under the hood 135 139 136 140 AppHerder reads the AppImage's filesystem to grab its icon and desktop entry. SquashFS images are parsed in-process. DwarFS images use the system dwarfsextract tool, falling back to the AppImage's own runtime when the tool isn't available. Everything it writes is tagged, so uninstall and sync only touch its own files.
+91
docs/Configuration.md
··· 1 + # Configuration 2 + 3 + AppHerder reads `~/.config/appherder/config.toml` on startup. Missing or malformed keys fall back to defaults. 4 + 5 + ## Example 6 + 7 + ```toml 8 + appimages_dir = "/data/AppImages" 9 + max_saved_versions = 5 10 + bin_dir = "/usr/local/bin" 11 + 12 + [sources.firefox] 13 + type = "github" 14 + owner = "mozilla" 15 + repo = "geckodriver" 16 + tag = "latest" 17 + pattern = "Firefox-*.AppImage" 18 + 19 + [sources.librewolf] 20 + type = "gitlab" 21 + host = "gitlab.com" 22 + project = "librewolf-community/browser/appimage" 23 + tag = "latest" 24 + pattern = "LibreWolf-*.AppImage" 25 + 26 + [sources.myapp] 27 + type = "static" 28 + url = "https://example.com/MyApp-latest.AppImage" 29 + ``` 30 + 31 + ## Settings 32 + 33 + | Key | Type | Default | Description | 34 + |---|---|---|---| 35 + | `appimages_dir` | string | `~/AppImages` | Directory where AppImages live. | 36 + | `max_saved_versions` | int | `3` | Number of prior versions kept for rollback. | 37 + | `bin_dir` | string | `~/.local/bin` | Directory for `appherder link` symlinks. | 38 + 39 + ## Source overrides 40 + 41 + The `[sources]` table overrides the update source for an app, taking priority over the embedded `.upd_info` ELF section. The key is the app name (the filename without `.appimage`). 42 + 43 + ### github 44 + 45 + GitHub Releases. 46 + 47 + ```toml 48 + [sources.appname] 49 + type = "github" 50 + owner = "owner" 51 + repo = "repo" 52 + tag = "latest" 53 + pattern = "*-x86_64.AppImage" 54 + ``` 55 + 56 + Set `GH_TOKEN` or `GITHUB_TOKEN` in the environment for higher API rate limits. 57 + 58 + ### gitlab 59 + 60 + GitLab Releases (gitlab.com or self-hosted). 61 + 62 + ```toml 63 + [sources.appname] 64 + type = "gitlab" 65 + host = "gitlab.com" 66 + project = "group/project" 67 + tag = "latest" 68 + pattern = "*-x86_64.AppImage" 69 + ``` 70 + 71 + Set `GL_TOKEN` or `GITLAB_TOKEN` in the environment for higher API rate limits. 72 + 73 + ### static 74 + 75 + A fixed URL that always serves the latest AppImage. 76 + 77 + ```toml 78 + [sources.appname] 79 + type = "static" 80 + url = "https://example.com/App-latest.AppImage" 81 + ``` 82 + 83 + ### zsync 84 + 85 + A zsync control file at a fixed URL. 86 + 87 + ```toml 88 + [sources.appname] 89 + type = "zsync" 90 + url = "https://example.com/App-latest.AppImage.zsync" 91 + ```
+40 -3
internal/appherder/config.go
··· 10 10 ) 11 11 12 12 type Config struct { 13 - AppImagesDir string `toml:"appimages_dir"` 14 - MaxSavedVersions int `toml:"max_saved_versions"` 15 - BinDir string `toml:"bin_dir"` 13 + AppImagesDir string `toml:"appimages_dir"` 14 + MaxSavedVersions int `toml:"max_saved_versions"` 15 + BinDir string `toml:"bin_dir"` 16 + Sources map[string]SourceConfig `toml:"sources"` 17 + } 18 + 19 + type SourceConfig struct { 20 + Type string `toml:"type"` 21 + Owner string `toml:"owner"` 22 + Repo string `toml:"repo"` 23 + Host string `toml:"host"` 24 + Project string `toml:"project"` 25 + Tag string `toml:"tag"` 26 + Pattern string `toml:"pattern"` 27 + URL string `toml:"url"` 28 + } 29 + 30 + func (sc SourceConfig) ToSource() (Source, error) { 31 + switch sc.Type { 32 + case "github": 33 + return githubReleaseSource{ 34 + owner: sc.Owner, 35 + repo: sc.Repo, 36 + tag: sc.Tag, 37 + pattern: sc.Pattern, 38 + }, nil 39 + case "gitlab": 40 + return gitlabReleaseSource{ 41 + host: sc.Host, 42 + project: sc.Project, 43 + tag: sc.Tag, 44 + pattern: sc.Pattern, 45 + }, nil 46 + case "zsync": 47 + return zsyncURLSource{url: sc.URL}, nil 48 + case "static": 49 + return staticURLSource{url: sc.URL}, nil 50 + default: 51 + return nil, fmt.Errorf("unknown source type %q (expected github, gitlab, zsync, or static)", sc.Type) 52 + } 16 53 } 17 54 18 55 func configPath() string {
+5 -7
internal/appherder/list.go
··· 31 31 32 32 infos := make([]AppInfo, 0, len(appids)) 33 33 for _, appid := range appids { 34 - infos = append(infos, gatherAppInfo(a.applicationsDir, a.appimagesDir, appid)) 34 + infos = append(infos, a.gatherAppInfo(appid)) 35 35 } 36 36 sort.Slice(infos, func(i, j int) bool { return infos[i].Name < infos[j].Name }) 37 37 return infos, nil 38 38 } 39 39 40 - // gatherAppInfo collects display metadata for appid from its installed desktop 41 - // file and AppImage. 42 - func gatherAppInfo(appsDir, appimagesDir, appid string) AppInfo { 40 + func (a App) gatherAppInfo(appid string) AppInfo { 43 41 info := AppInfo{AppID: appid, Source: "none"} 44 42 45 43 var pinned string 46 - if desktop, err := desktopfile.Read(filepath.Join(appsDir, appid+".desktop")); err == nil { 44 + if desktop, err := desktopfile.Read(filepath.Join(a.applicationsDir, appid+".desktop")); err == nil { 47 45 if name, ok := desktop.Get(desktopEntrySection, "Name"); ok && name != "" { 48 46 info.Name = name 49 47 } ··· 57 55 } 58 56 59 57 var signed bool 60 - if path, err := findAppImagePath(appimagesDir, appid); err == nil && path != "" { 58 + if path, err := findAppImagePath(a.appimagesDir, appid); err == nil && path != "" { 61 59 info.Path = path 62 60 info.Filename = filepath.Base(path) 63 61 if stat, err := os.Stat(path); err == nil { 64 62 info.Size = stat.Size() 65 63 } 66 - if src, err := SourceForAppImage(path); err == nil && src != nil { 64 + if src, err := a.SourceForAppImage(path); err == nil && src != nil { 67 65 info.Source = src.Kind() 68 66 } 69 67 signed = appImageSigned(path)
+15 -3
internal/appherder/source.go
··· 12 12 "io" 13 13 "os" 14 14 "path" 15 + "path/filepath" 15 16 "sort" 16 17 "strings" 17 18 "time" ··· 97 98 return string(data), nil 98 99 } 99 100 100 - // SourceForAppImage resolves an update source from the AppImage's embedded 101 - // update info. It returns (nil, nil) when the AppImage carries none. 102 - func SourceForAppImage(file string) (Source, error) { 101 + // SourceForAppImage resolves an update source for the given AppImage, checking 102 + // config.toml's [sources] table first, then falling back to the embedded 103 + // .upd_info ELF section. Returns (nil, nil) when no source is configured. 104 + func (a App) SourceForAppImage(file string) (Source, error) { 105 + name := strings.TrimSuffix(filepath.Base(file), filepath.Ext(file)) 106 + if sc, ok := a.config.Sources[name]; ok { 107 + return sc.ToSource() 108 + } 109 + return sourceFromELF(file) 110 + } 111 + 112 + // sourceFromELF reads the AppImage's embedded .upd_info ELF section and 113 + // returns a source, or (nil, nil) when the AppImage carries none. 114 + func sourceFromELF(file string) (Source, error) { 103 115 info, err := ReadUpdateInfo(file) 104 116 if err != nil { 105 117 return nil, err
+3 -6
internal/appherder/upgrade.go
··· 50 50 } 51 51 } 52 52 53 - return parallelMap(ctx, managed, checkConcurrency, func(ctx context.Context, file string) UpgradeCheck { 54 - return checkOne(ctx, file) 55 - }), nil 53 + return parallelMap(ctx, managed, checkConcurrency, a.checkOne), nil 56 54 } 57 55 58 56 // ApplyUpgrades downloads and installs updates for the given checks, processing ··· 74 72 return applied 75 73 } 76 74 77 - // checkOne resolves an AppImage's source and reports whether an update exists. 78 - func checkOne(ctx context.Context, file string) UpgradeCheck { 75 + func (a App) checkOne(ctx context.Context, file string) UpgradeCheck { 79 76 name := strings.TrimSuffix(filepath.Base(file), filepath.Ext(file)) 80 77 81 - src, err := SourceForAppImage(file) 78 + src, err := a.SourceForAppImage(file) 82 79 if err != nil { 83 80 return UpgradeCheck{Name: name, Err: err} 84 81 }