A shepherd for your Appimages.
0

Configure Feed

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

appherder-gui: check for/perform updates

Aly Raffauf (Jun 19, 2026, 4:33 AM EDT) db9b96b5 85418ef0

+209 -18
+209 -18
cmd/appherder-gui/main_window.go
··· 21 21 type mainWindow struct { 22 22 *adw.ApplicationWindow 23 23 24 - app appherder.App 25 - split *adw.NavigationSplitView 26 - apps []appherder.AppInfo 27 - list *gtk.ListBox 28 - installBtn *gtk.MenuButton 29 - menuBtn *gtk.MenuButton 30 - busy int 24 + app appherder.App 25 + split *adw.NavigationSplitView 26 + apps []appherder.AppInfo 27 + updateChecks map[string]appherder.UpgradeCheck 28 + checkingUpdates bool 29 + checkedUpdates bool 30 + currentKey string 31 + list *gtk.ListBox 32 + installBtn *gtk.MenuButton 33 + menuBtn *gtk.MenuButton 34 + checkAction *gio.SimpleAction 35 + updateAllAction *gio.SimpleAction 36 + busy int 31 37 } 32 38 33 39 func newMainWindow(gtkApp *adw.Application, app appherder.App) *mainWindow { ··· 55 61 win.SetContent(win.split) 56 62 57 63 win.loadApps() 64 + win.checkUpdates(false) 58 65 return win 59 66 } 60 67 ··· 133 140 134 141 menu := gio.NewMenu() 135 142 updateSection := gio.NewMenu() 143 + updateSection.Append("Check for Updates", "win.check-upgrades") 136 144 updateSection.Append("Update All", "win.apply-upgrades") 137 145 menu.AppendSection("", updateSection) 138 146 aboutSection := gio.NewMenu() ··· 140 148 menu.AppendSection("", aboutSection) 141 149 w.menuBtn.SetMenuModel(menu) 142 150 143 - w.addAction("apply-upgrades", w.applyUpgrades) 151 + w.checkAction = w.addAction("check-upgrades", func() { w.checkUpdates(true) }) 152 + w.updateAllAction = w.addAction("apply-upgrades", w.applyUpgrades) 144 153 w.addAction("install-file", w.promptInstallFile) 145 154 w.addAction("install-url", w.promptInstallURL) 146 155 w.addAction("about", w.showAbout) 156 + w.updateActionSensitivity() 147 157 } 148 158 149 - func (w *mainWindow) addAction(name string, activate func()) { 159 + func (w *mainWindow) addAction(name string, activate func()) *gio.SimpleAction { 150 160 action := gio.NewSimpleAction(name, nil) 151 161 action.ConnectActivate(func(parameter *glib.Variant) { 152 162 activate() 153 163 }) 154 164 w.AddAction(action) 165 + return action 155 166 } 156 167 157 168 func (w *mainWindow) showAbout() { ··· 186 197 w.list.RemoveAll() 187 198 if len(infos) == 0 { 188 199 w.apps = nil 200 + w.currentKey = "" 189 201 w.split.SetContent(adw.NewNavigationPage(emptyDetailsPage(), "AppHerder")) 190 202 return 191 203 } 204 + selected := w.currentKey 192 205 for i, info := range infos { 193 - w.list.Append(appListRow(info)) 194 - if i == 0 { 206 + w.list.Append(w.appListRow(info)) 207 + if selected == "" && i == 0 { 195 208 w.showDetails(info, false) 196 209 } 197 210 } 198 - if first := w.list.RowAtIndex(0); first != nil { 199 - w.list.SelectRow(first) 211 + rowIndex := 0 212 + if selected != "" { 213 + for i, info := range infos { 214 + if appKey(info) == selected { 215 + rowIndex = i 216 + break 217 + } 218 + } 219 + } 220 + if row := w.list.RowAtIndex(rowIndex); row != nil { 221 + w.list.SelectRow(row) 200 222 } 201 223 } 202 224 ··· 219 241 w.showDetails(w.apps[index], reveal) 220 242 } 221 243 222 - func appListRow(info appherder.AppInfo) *gtk.ListBoxRow { 244 + func (w *mainWindow) appListRow(info appherder.AppInfo) *gtk.ListBoxRow { 223 245 row := gtk.NewListBoxRow() 224 246 row.SetActivatable(true) 225 247 row.SetSelectable(true) ··· 242 264 name.SetVAlign(gtk.AlignCenter) 243 265 box.Append(name) 244 266 267 + if w.updateAvailable(info) { 268 + indicator := gtk.NewLabel("Update") 269 + indicator.AddCSSClass("caption") 270 + indicator.AddCSSClass("dim-label") 271 + indicator.SetVAlign(gtk.AlignCenter) 272 + indicator.SetMarginStart(6) 273 + indicator.SetTooltipText("Update Available") 274 + box.Append(indicator) 275 + } 276 + 245 277 row.SetChild(box) 246 278 return row 247 279 } ··· 254 286 } 255 287 256 288 func (w *mainWindow) showDetails(info appherder.AppInfo, reveal bool) { 289 + w.currentKey = appKey(info) 257 290 w.split.SetContent(adw.NewNavigationPage(w.appDetailsView(info), info.Name)) 258 291 if reveal { 259 292 w.split.SetShowContent(true) ··· 299 332 launch.ConnectClicked(func() { w.launchApp(info) }) 300 333 actions.Append(launch) 301 334 335 + if check, ok := w.updateCheck(info); ok && check.Available { 336 + update := gtk.NewButtonWithLabel("Update") 337 + update.AddCSSClass("pill") 338 + update.ConnectClicked(func() { w.updateApp(info) }) 339 + actions.Append(update) 340 + } 341 + 302 342 remove := gtk.NewButtonWithLabel("Remove") 303 343 remove.AddCSSClass("pill") 304 344 remove.AddCSSClass("destructive-action") ··· 315 355 label string 316 356 value string 317 357 }{ 358 + {"Update Status", w.updateStatus(info)}, 318 359 {"Version", orDash(info.Version)}, 319 360 {"Size", sizeOrDash(info.Size)}, 320 361 {"Update Source", sourceLabel(info.Source)}, ··· 367 408 return image 368 409 } 369 410 411 + func (w *mainWindow) checkUpdates(userInitiated bool) { 412 + if w.checkingUpdates { 413 + return 414 + } 415 + w.checkingUpdates = true 416 + w.refreshVisibleApps() 417 + w.updateActionSensitivity() 418 + 419 + go func() { 420 + checks, err := w.app.CheckUpgrades(context.Background()) 421 + w.idle(func() { 422 + w.checkingUpdates = false 423 + if err != nil { 424 + if userInitiated { 425 + w.showError("Could Not Check for Updates", err.Error()) 426 + } 427 + w.updateActionSensitivity() 428 + w.refreshVisibleApps() 429 + return 430 + } 431 + w.updateChecks = checksByName(checks) 432 + w.checkedUpdates = true 433 + w.updateActionSensitivity() 434 + w.refreshVisibleApps() 435 + }) 436 + }() 437 + } 438 + 370 439 func (w *mainWindow) applyUpgrades() { 371 440 w.run("Installing updates", func() (string, error) { 372 - checks, err := w.app.CheckUpgrades(context.Background()) 373 - if err != nil { 374 - return "", err 441 + checks := w.availableChecks() 442 + if len(checks) == 0 { 443 + var err error 444 + checks, err = w.app.CheckUpgrades(context.Background()) 445 + if err != nil { 446 + return "", err 447 + } 375 448 } 376 449 applied := w.app.ApplyUpgrades(context.Background(), checks) 377 450 upgraded, failed := summarizeApplied(applied) 378 - w.idle(w.loadApps) 451 + w.idle(func() { 452 + w.updateChecks = nil 453 + w.checkedUpdates = false 454 + w.loadApps() 455 + w.checkUpdates(false) 456 + }) 379 457 if upgraded == 0 && failed == 0 { 380 458 return "Everything is up to date", nil 381 459 } ··· 383 461 }) 384 462 } 385 463 464 + func (w *mainWindow) updateApp(info appherder.AppInfo) { 465 + check, ok := w.updateCheck(info) 466 + if !ok || !check.Available { 467 + return 468 + } 469 + w.run("Updating "+info.Name, func() (string, error) { 470 + applied := w.app.ApplyUpgrades(context.Background(), []appherder.UpgradeCheck{check}) 471 + if len(applied) == 0 { 472 + return info.Name + " is up to date", nil 473 + } 474 + if applied[0].Err != nil { 475 + return "", applied[0].Err 476 + } 477 + w.idle(func() { 478 + w.updateChecks = nil 479 + w.checkedUpdates = false 480 + w.loadApps() 481 + w.checkUpdates(false) 482 + }) 483 + return fmt.Sprintf("Updated %s to %s", info.Name, applied[0].Version), nil 484 + }) 485 + } 486 + 386 487 func (w *mainWindow) launchApp(info appherder.AppInfo) { 387 488 w.run("Launching "+info.Name, func() (string, error) { 388 489 if err := w.app.Launch(appKey(info)); err != nil { ··· 539 640 sensitive := w.busy == 0 540 641 w.installBtn.SetSensitive(sensitive) 541 642 w.menuBtn.SetSensitive(sensitive) 643 + w.updateActionSensitivity() 644 + } 645 + 646 + func (w *mainWindow) updateActionSensitivity() { 647 + if w.checkAction != nil { 648 + w.checkAction.SetEnabled(w.busy == 0 && !w.checkingUpdates) 649 + } 650 + if w.updateAllAction != nil { 651 + w.updateAllAction.SetEnabled(w.busy == 0 && !w.checkingUpdates && len(w.availableChecks()) > 0) 652 + } 653 + } 654 + 655 + func (w *mainWindow) refreshVisibleApps() { 656 + if len(w.apps) == 0 { 657 + return 658 + } 659 + w.renderApps(w.apps) 660 + if w.currentKey == "" { 661 + return 662 + } 663 + for _, info := range w.apps { 664 + if appKey(info) == w.currentKey { 665 + w.showDetails(info, false) 666 + return 667 + } 668 + } 542 669 } 543 670 544 671 func (w *mainWindow) idle(fn func()) { ··· 562 689 return strings.EqualFold(filepath.Ext(path), ".appimage") 563 690 } 564 691 692 + func (w *mainWindow) updateAvailable(info appherder.AppInfo) bool { 693 + check, ok := w.updateCheck(info) 694 + return ok && check.Available 695 + } 696 + 697 + func (w *mainWindow) updateStatus(info appherder.AppInfo) string { 698 + if w.checkingUpdates { 699 + return "Checking for updates" 700 + } 701 + check, ok := w.updateCheck(info) 702 + if !ok { 703 + if w.checkedUpdates { 704 + return "Not managed by AppHerder" 705 + } 706 + return "Not checked yet" 707 + } 708 + if check.Err != nil { 709 + return "Could not check" 710 + } 711 + if check.NoSource { 712 + return "Unavailable" 713 + } 714 + if check.Available { 715 + return "Update available: " + orDash(check.Release.Version) 716 + } 717 + return "Up to date" 718 + } 719 + 720 + func (w *mainWindow) updateCheck(info appherder.AppInfo) (appherder.UpgradeCheck, bool) { 721 + if w.updateChecks == nil { 722 + return appherder.UpgradeCheck{}, false 723 + } 724 + check, ok := w.updateChecks[upgradeKey(info)] 725 + return check, ok 726 + } 727 + 728 + func (w *mainWindow) availableChecks() []appherder.UpgradeCheck { 729 + if w.updateChecks == nil { 730 + return nil 731 + } 732 + checks := make([]appherder.UpgradeCheck, 0, len(w.updateChecks)) 733 + for _, check := range w.updateChecks { 734 + if check.Err == nil && !check.NoSource && check.Available { 735 + checks = append(checks, check) 736 + } 737 + } 738 + return checks 739 + } 740 + 741 + func checksByName(checks []appherder.UpgradeCheck) map[string]appherder.UpgradeCheck { 742 + byName := make(map[string]appherder.UpgradeCheck, len(checks)) 743 + for _, check := range checks { 744 + byName[check.Name] = check 745 + } 746 + return byName 747 + } 748 + 565 749 func summarizeApplied(applied []appherder.UpgradeApplied) (upgraded, failed int) { 566 750 for _, app := range applied { 567 751 if app.Err != nil { ··· 608 792 return appherder.NormalizeAppName(info.Name) 609 793 } 610 794 return strings.TrimSuffix(filepath.Base(info.Filename), filepath.Ext(info.Filename)) 795 + } 796 + 797 + func upgradeKey(info appherder.AppInfo) string { 798 + if info.Filename != "" { 799 + return strings.TrimSuffix(filepath.Base(info.Filename), filepath.Ext(info.Filename)) 800 + } 801 + return appKey(info) 611 802 } 612 803 613 804 func sizeOrDash(bytes int64) string {