A shepherd for your Appimages.
0

Configure Feed

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

add download progress bars

Aly Raffauf (Jun 18, 2026, 2:22 PM EDT) 2634b25e 537f2a56

+89 -11
+6 -3
cmd/appherder/cli.go
··· 52 52 var name string 53 53 var err error 54 54 if isURL(arg) { 55 - fmt.Fprintf(cmd.OutOrStdout(), "downloading %s...\n", arg) 56 - name, err = a.InstallFromURL(cmd.Context(), arg) 55 + progress := newCliProgress(cmd.ErrOrStderr()) 56 + name, err = a.WithProgress(progress).InstallFromURL(cmd.Context(), arg) 57 + progress.Done() 57 58 } else { 58 59 name, err = a.Install(arg) 59 60 } ··· 168 169 printUpgradeChecks(out, checks) 169 170 return nil 170 171 } 171 - applied := a.ApplyUpgrades(cmd.Context(), checks) 172 + progress := newCliProgress(cmd.ErrOrStderr()) 173 + applied := a.WithProgress(progress).ApplyUpgrades(cmd.Context(), checks) 174 + progress.Done() 172 175 printUpgradeApplied(out, checks, applied) 173 176 return nil 174 177 },
+34
cmd/appherder/progress.go
··· 1 + package main 2 + 3 + import ( 4 + "fmt" 5 + "io" 6 + ) 7 + 8 + type cliProgress struct { 9 + out io.Writer 10 + } 11 + 12 + func newCliProgress(out io.Writer) *cliProgress { 13 + return &cliProgress{out: out} 14 + } 15 + 16 + func (p *cliProgress) Download(name string, received, total int64) { 17 + if total > 0 { 18 + percent := received * 100 / total 19 + fmt.Fprintf(p.out, "\r%s: %s / %s (%d%%)", truncate(name), humanSize(received), humanSize(total), percent) 20 + return 21 + } 22 + fmt.Fprintf(p.out, "\r%s: %s", truncate(name), humanSize(received)) 23 + } 24 + 25 + func (p *cliProgress) Done() { 26 + fmt.Fprint(p.out, "\r\033[K") 27 + } 28 + 29 + func truncate(name string) string { 30 + if len(name) > 20 { 31 + return name[:17] + "..." 32 + } 33 + return name 34 + }
+7
internal/appherder/app.go
··· 15 15 applicationsDir string 16 16 iconsDir string 17 17 binDir string 18 + progress Progress 18 19 } 19 20 20 21 // NewApp returns an App wired to the current user's home directory. The ··· 43 44 binDir: binDir, 44 45 } 45 46 } 47 + 48 + // WithProgress returns a copy of App that reports download progress to p. 49 + func (a App) WithProgress(p Progress) App { 50 + a.progress = p 51 + return a 52 + }
+1 -1
internal/appherder/install.go
··· 106 106 107 107 // InstallFromURL downloads an AppImage from url and installs it. 108 108 func (a App) InstallFromURL(ctx context.Context, url string) (string, error) { 109 - tmpName, err := downloadToTemp(ctx, url, "appherder-install") 109 + tmpName, err := downloadToTemp(ctx, url, "appherder-install", a.progress, url) 110 110 if err != nil { 111 111 return "", err 112 112 }
+31
internal/appherder/progress.go
··· 1 + package appherder 2 + 3 + import "io" 4 + 5 + // Progress receives download progress updates. total is -1 when the server 6 + // reports no content length. 7 + type Progress interface { 8 + Download(name string, received, total int64) 9 + } 10 + 11 + type progressReader struct { 12 + reader io.Reader 13 + progress Progress 14 + name string 15 + total int64 16 + received int64 17 + } 18 + 19 + func newProgressReader(reader io.Reader, progress Progress, name string, total int64) io.Reader { 20 + if progress == nil { 21 + return reader 22 + } 23 + return &progressReader{reader: reader, progress: progress, name: name, total: total} 24 + } 25 + 26 + func (pr *progressReader) Read(buf []byte) (int, error) { 27 + bytesRead, err := pr.reader.Read(buf) 28 + pr.received += int64(bytesRead) 29 + pr.progress.Download(pr.name, pr.received, pr.total) 30 + return bytesRead, err 31 + }
+10 -7
internal/appherder/upgrade.go
··· 53 53 if check.Err != nil || check.NoSource || !check.Available { 54 54 continue 55 55 } 56 - err := a.applyUpgrade(ctx, check.Release) 56 + err := a.applyUpgrade(ctx, check.Name, check.Release) 57 57 applied = append(applied, UpgradeApplied{ 58 58 Name: check.Name, 59 59 Version: check.Release.Version, ··· 109 109 return results 110 110 } 111 111 112 - func (a App) applyUpgrade(ctx context.Context, rel Release) error { 113 - tmpName, err := downloadToTemp(ctx, rel.URL, "appherder-upgrade") 112 + func (a App) applyUpgrade(ctx context.Context, name string, rel Release) error { 113 + tmpName, err := downloadToTemp(ctx, rel.URL, "appherder-upgrade", a.progress, name) 114 114 if err != nil { 115 115 return err 116 116 } ··· 122 122 123 123 // downloadToTemp downloads url to a temporary file and returns its path. The 124 124 // caller must remove the file. 125 - func downloadToTemp(ctx context.Context, url, prefix string) (string, error) { 125 + func downloadToTemp(ctx context.Context, url, prefix string, progress Progress, name string) (string, error) { 126 126 tmp, err := os.CreateTemp("", prefix+"-*.appimage") 127 127 if err != nil { 128 128 return "", fmt.Errorf("create temporary file: %w", err) 129 129 } 130 130 tmpName := tmp.Name() 131 - if err := download(ctx, url, tmp); err != nil { 131 + if err := download(ctx, url, tmp, progress, name); err != nil { 132 132 tmp.Close() 133 133 os.Remove(tmpName) 134 134 return "", err ··· 140 140 return tmpName, nil 141 141 } 142 142 143 - func download(ctx context.Context, url string, writer io.Writer) error { 143 + func download(ctx context.Context, url string, writer io.Writer, progress Progress, name string) error { 144 144 ctx, cancel := context.WithCancel(ctx) 145 145 defer cancel() 146 146 ··· 149 149 return err 150 150 } 151 151 defer resp.Body.Close() 152 - _, err = io.Copy(writer, newIdleTimeoutReader(resp.Body, downloadIdleTimeout, cancel)) 152 + 153 + body := io.Reader(newIdleTimeoutReader(resp.Body, downloadIdleTimeout, cancel)) 154 + body = newProgressReader(body, progress, name, resp.ContentLength) 155 + _, err = io.Copy(writer, body) 153 156 return err 154 157 }