A shepherd for your Appimages.
0

Configure Feed

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

basic adwaita GUI (#1)

* init

* cleanup sidebar

* add build helpers

authored by

Aly Raffauf and committed by
GitHub
(Jun 19, 2026, 2:59 AM EDT) 53d53e14 6f82663f

+742 -3
+35
cmd/appherder-gui/main.go
··· 1 + //go:build gtk 2 + 3 + package main 4 + 5 + import ( 6 + "fmt" 7 + "os" 8 + 9 + "github.com/alyraffauf/appherder/internal/appherder" 10 + "github.com/diamondburned/gotk4-adwaita/pkg/adw" 11 + "github.com/diamondburned/gotk4/pkg/gio/v2" 12 + ) 13 + 14 + var version = "dev" 15 + 16 + func main() { 17 + app, err := appherder.NewApp() 18 + if err != nil { 19 + fmt.Fprintln(os.Stderr, err) 20 + os.Exit(1) 21 + } 22 + 23 + appID := "io.github.alyraffauf.AppHerder" 24 + gtkApp := adw.NewApplication(appID, gio.ApplicationFlagsNone) 25 + gtkApp.SetVersion(version) 26 + 27 + gtkApp.ConnectActivate(func() { 28 + window := newMainWindow(gtkApp, app) 29 + window.Present() 30 + }) 31 + 32 + if code := gtkApp.Run(nil); code > 0 { 33 + os.Exit(code) 34 + } 35 + }
+609
cmd/appherder-gui/main_window.go
··· 1 + //go:build gtk 2 + 3 + package main 4 + 5 + import ( 6 + "context" 7 + "fmt" 8 + "net/url" 9 + "path/filepath" 10 + "strings" 11 + 12 + "github.com/alyraffauf/appherder/internal/appherder" 13 + "github.com/diamondburned/gotk4-adwaita/pkg/adw" 14 + "github.com/diamondburned/gotk4/pkg/gio/v2" 15 + "github.com/diamondburned/gotk4/pkg/glib/v2" 16 + "github.com/diamondburned/gotk4/pkg/gtk/v4" 17 + "github.com/diamondburned/gotk4/pkg/pango" 18 + ) 19 + 20 + type mainWindow struct { 21 + *adw.ApplicationWindow 22 + 23 + app appherder.App 24 + split *adw.NavigationSplitView 25 + apps []appherder.AppInfo 26 + list *gtk.ListBox 27 + installBtn *gtk.MenuButton 28 + menuBtn *gtk.MenuButton 29 + busy int 30 + } 31 + 32 + func newMainWindow(gtkApp *adw.Application, app appherder.App) *mainWindow { 33 + win := &mainWindow{ 34 + ApplicationWindow: adw.NewApplicationWindow(&gtkApp.Application), 35 + app: app, 36 + split: adw.NewNavigationSplitView(), 37 + list: gtk.NewListBox(), 38 + installBtn: gtk.NewMenuButton(), 39 + menuBtn: gtk.NewMenuButton(), 40 + } 41 + 42 + win.SetTitle("AppHerder") 43 + win.SetDefaultSize(760, 520) 44 + win.installActions() 45 + 46 + win.split.SetShowContent(true) 47 + win.split.SetMinSidebarWidth(260) 48 + win.split.SetMaxSidebarWidth(300) 49 + win.split.SetSidebar(adw.NewNavigationPage(win.sidebarView(), "AppHerder")) 50 + win.split.SetContent(adw.NewNavigationPage(emptyDetailsPage(), "AppHerder")) 51 + win.installBreakpoints() 52 + 53 + win.SetContent(win.split) 54 + 55 + win.loadApps() 56 + return win 57 + } 58 + 59 + func (w *mainWindow) installBreakpoints() { 60 + condition := adw.NewBreakpointConditionLength(adw.BreakpointConditionMaxWidth, 600, adw.LengthUnitSp) 61 + breakpoint := adw.NewBreakpoint(condition) 62 + breakpoint.AddSetterDirect(w.split.Object, "collapsed", glib.NewValue(true)) 63 + breakpoint.AddSetterDirect(w.split.Object, "show-content", glib.NewValue(false)) 64 + w.AddBreakpoint(breakpoint) 65 + } 66 + 67 + func (w *mainWindow) sidebarView() *adw.ToolbarView { 68 + title := adw.NewWindowTitle("AppHerder", "") 69 + header := adw.NewHeaderBar() 70 + header.SetShowEndTitleButtons(false) 71 + header.SetTitleWidget(title) 72 + header.PackStart(w.installBtn) 73 + header.PackEnd(w.menuBtn) 74 + 75 + w.list.AddCSSClass("navigation-sidebar") 76 + w.list.SetSelectionMode(gtk.SelectionSingle) 77 + w.list.ConnectRowActivated(func(row *gtk.ListBoxRow) { 78 + w.activateAppRow(row) 79 + }) 80 + w.list.ConnectRowSelected(func(row *gtk.ListBoxRow) { 81 + w.selectAppRow(row) 82 + }) 83 + 84 + scroller := gtk.NewScrolledWindow() 85 + scroller.SetPolicy(gtk.PolicyNever, gtk.PolicyAutomatic) 86 + scroller.SetChild(w.list) 87 + scroller.SetVExpand(true) 88 + 89 + toolbar := adw.NewToolbarView() 90 + toolbar.AddTopBar(header) 91 + toolbar.SetContent(scroller) 92 + return toolbar 93 + } 94 + 95 + func emptyDetailsPage() *adw.ToolbarView { 96 + header := adw.NewHeaderBar() 97 + 98 + empty := adw.NewStatusPage() 99 + empty.SetIconName("application-x-executable-symbolic") 100 + empty.SetTitle("No AppImage Selected") 101 + empty.SetDescription("Select an installed AppImage to view details.") 102 + 103 + toolbar := adw.NewToolbarView() 104 + toolbar.AddTopBar(header) 105 + toolbar.SetContent(empty) 106 + return toolbar 107 + } 108 + 109 + func (w *mainWindow) installActions() { 110 + w.installBtn.SetIconName("list-add-symbolic") 111 + w.installBtn.SetTooltipText("Install AppImage") 112 + installMenu := gio.NewMenu() 113 + installMenu.Append("Install from File", "win.install-file") 114 + installMenu.Append("Install from URL", "win.install-url") 115 + w.installBtn.SetMenuModel(installMenu) 116 + 117 + w.menuBtn.SetIconName("open-menu-symbolic") 118 + w.menuBtn.SetTooltipText("Main Menu") 119 + 120 + menu := gio.NewMenu() 121 + updateSection := gio.NewMenu() 122 + updateSection.Append("Update All", "win.apply-upgrades") 123 + menu.AppendSection("", updateSection) 124 + aboutSection := gio.NewMenu() 125 + aboutSection.Append("About AppHerder", "win.about") 126 + menu.AppendSection("", aboutSection) 127 + w.menuBtn.SetMenuModel(menu) 128 + 129 + w.addAction("apply-upgrades", w.applyUpgrades) 130 + w.addAction("install-file", w.promptInstallFile) 131 + w.addAction("install-url", w.promptInstallURL) 132 + w.addAction("about", w.showAbout) 133 + } 134 + 135 + func (w *mainWindow) addAction(name string, activate func()) { 136 + action := gio.NewSimpleAction(name, nil) 137 + action.ConnectActivate(func(parameter *glib.Variant) { 138 + activate() 139 + }) 140 + w.AddAction(action) 141 + } 142 + 143 + func (w *mainWindow) showAbout() { 144 + about := adw.NewAboutDialog() 145 + about.SetApplicationName("AppHerder") 146 + about.SetApplicationIcon("application-x-executable-symbolic") 147 + about.SetVersion(version) 148 + about.SetDeveloperName("Aly Raffauf") 149 + about.SetDevelopers([]string{"Aly Raffauf"}) 150 + about.SetComments("Manage AppImages installed in your home directory.") 151 + about.SetWebsite("https://github.com/alyraffauf/appherder") 152 + about.SetIssueURL("https://github.com/alyraffauf/appherder/issues") 153 + about.SetLicenseType(gtk.LicenseGPL30) 154 + about.Present(w) 155 + } 156 + 157 + func (w *mainWindow) loadApps() { 158 + w.run("Refreshing app list", func() (string, error) { 159 + infos, err := w.app.List() 160 + if err != nil { 161 + return "", err 162 + } 163 + w.idle(func() { 164 + w.renderApps(infos) 165 + }) 166 + return "", nil 167 + }) 168 + } 169 + 170 + func (w *mainWindow) renderApps(infos []appherder.AppInfo) { 171 + w.apps = infos 172 + w.list.RemoveAll() 173 + if len(infos) == 0 { 174 + w.apps = nil 175 + w.split.SetContent(adw.NewNavigationPage(emptyDetailsPage(), "AppHerder")) 176 + return 177 + } 178 + for i, info := range infos { 179 + w.list.Append(appListRow(info)) 180 + if i == 0 { 181 + w.showDetails(info, false) 182 + } 183 + } 184 + if first := w.list.RowAtIndex(0); first != nil { 185 + w.list.SelectRow(first) 186 + } 187 + } 188 + 189 + func (w *mainWindow) activateAppRow(row *gtk.ListBoxRow) { 190 + w.showDetailsForRow(row, true) 191 + } 192 + 193 + func (w *mainWindow) selectAppRow(row *gtk.ListBoxRow) { 194 + w.showDetailsForRow(row, false) 195 + } 196 + 197 + func (w *mainWindow) showDetailsForRow(row *gtk.ListBoxRow, reveal bool) { 198 + if row == nil { 199 + return 200 + } 201 + index := row.Index() 202 + if index < 0 || index >= len(w.apps) { 203 + return 204 + } 205 + w.showDetails(w.apps[index], reveal) 206 + } 207 + 208 + func appListRow(info appherder.AppInfo) *gtk.ListBoxRow { 209 + row := gtk.NewListBoxRow() 210 + row.SetActivatable(true) 211 + row.SetSelectable(true) 212 + 213 + box := gtk.NewBox(gtk.OrientationHorizontal, 12) 214 + box.SetMarginTop(8) 215 + box.SetMarginBottom(8) 216 + box.SetMarginStart(12) 217 + box.SetMarginEnd(12) 218 + 219 + icon := appIcon(info, 36) 220 + icon.SetVAlign(gtk.AlignCenter) 221 + box.Append(icon) 222 + 223 + name := gtk.NewLabel(info.Name) 224 + name.SetXAlign(0) 225 + name.SetEllipsize(pango.EllipsizeEnd) 226 + name.SetLines(1) 227 + name.SetHExpand(true) 228 + name.SetVAlign(gtk.AlignCenter) 229 + box.Append(name) 230 + 231 + row.SetChild(box) 232 + return row 233 + } 234 + 235 + func appSummary(info appherder.AppInfo) string { 236 + if info.Filename != "" { 237 + return info.Filename 238 + } 239 + return info.AppID 240 + } 241 + 242 + func (w *mainWindow) showDetails(info appherder.AppInfo, reveal bool) { 243 + w.split.SetContent(adw.NewNavigationPage(w.appDetailsView(info), info.Name)) 244 + if reveal { 245 + w.split.SetShowContent(true) 246 + } 247 + } 248 + 249 + func (w *mainWindow) appDetailsView(info appherder.AppInfo) *adw.ToolbarView { 250 + header := adw.NewHeaderBar() 251 + 252 + hero := gtk.NewBox(gtk.OrientationVertical, 8) 253 + hero.SetMarginTop(24) 254 + hero.SetMarginBottom(20) 255 + hero.SetMarginStart(24) 256 + hero.SetMarginEnd(24) 257 + hero.SetHAlign(gtk.AlignCenter) 258 + hero.Append(appIcon(info, 88)) 259 + 260 + summary := gtk.NewBox(gtk.OrientationVertical, 4) 261 + summary.SetHAlign(gtk.AlignCenter) 262 + 263 + name := gtk.NewLabel(info.Name) 264 + name.AddCSSClass("title-1") 265 + name.SetEllipsize(pango.EllipsizeEnd) 266 + name.SetMaxWidthChars(28) 267 + name.SetLines(1) 268 + 269 + subtitle := gtk.NewLabel(appSummary(info)) 270 + subtitle.SetWrap(true) 271 + subtitle.AddCSSClass("dim-label") 272 + subtitle.SetEllipsize(pango.EllipsizeEnd) 273 + subtitle.SetMaxWidthChars(34) 274 + subtitle.SetLines(1) 275 + 276 + summary.Append(name) 277 + summary.Append(subtitle) 278 + 279 + actions := gtk.NewBox(gtk.OrientationHorizontal, 8) 280 + actions.SetMarginTop(10) 281 + 282 + launch := gtk.NewButtonWithLabel("Launch") 283 + launch.AddCSSClass("pill") 284 + launch.AddCSSClass("suggested-action") 285 + launch.ConnectClicked(func() { w.launchApp(info) }) 286 + actions.Append(launch) 287 + 288 + remove := gtk.NewButtonWithLabel("Remove") 289 + remove.AddCSSClass("pill") 290 + remove.AddCSSClass("destructive-action") 291 + remove.ConnectClicked(func() { w.confirmUninstall(info) }) 292 + actions.Append(remove) 293 + 294 + summary.Append(actions) 295 + hero.Append(summary) 296 + 297 + details := adw.NewPreferencesGroup() 298 + details.SetTitle("Details") 299 + 300 + for _, field := range []struct { 301 + label string 302 + value string 303 + }{ 304 + {"Version", orDash(info.Version)}, 305 + {"Size", sizeOrDash(info.Size)}, 306 + {"Update Source", sourceLabel(info.Source)}, 307 + {"Signature", signatureLabel(info.Signature)}, 308 + {"App ID", info.AppID}, 309 + {"File", orDash(info.Filename)}, 310 + } { 311 + row := adw.NewActionRow() 312 + row.SetTitle(field.label) 313 + row.SetSubtitle(field.value) 314 + details.Add(row) 315 + } 316 + 317 + content := gtk.NewBox(gtk.OrientationVertical, 0) 318 + content.Append(hero) 319 + 320 + clamp := adw.NewClamp() 321 + clamp.SetMaximumSize(560) 322 + clamp.SetTighteningThreshold(520) 323 + clamp.SetChild(details) 324 + clamp.SetMarginStart(24) 325 + clamp.SetMarginEnd(24) 326 + clamp.SetMarginBottom(24) 327 + content.Append(clamp) 328 + 329 + scroller := gtk.NewScrolledWindow() 330 + scroller.SetChild(content) 331 + scroller.SetVExpand(true) 332 + 333 + toolbar := adw.NewToolbarView() 334 + toolbar.AddTopBar(header) 335 + toolbar.SetContent(scroller) 336 + return toolbar 337 + } 338 + 339 + func appIcon(info appherder.AppInfo, size int) *gtk.Image { 340 + if info.Icon != "" && strings.HasPrefix(info.Icon, "/") { 341 + image := gtk.NewImageFromFile(info.Icon) 342 + image.SetPixelSize(size) 343 + image.SetSizeRequest(size, size) 344 + return image 345 + } 346 + iconName := info.Icon 347 + if iconName == "" { 348 + iconName = "application-x-executable-symbolic" 349 + } 350 + image := gtk.NewImageFromIconName(iconName) 351 + image.SetPixelSize(size) 352 + image.SetSizeRequest(size, size) 353 + return image 354 + } 355 + 356 + func (w *mainWindow) applyUpgrades() { 357 + w.run("Installing updates", func() (string, error) { 358 + checks, err := w.app.CheckUpgrades(context.Background()) 359 + if err != nil { 360 + return "", err 361 + } 362 + applied := w.app.ApplyUpgrades(context.Background(), checks) 363 + upgraded, failed := summarizeApplied(applied) 364 + w.idle(w.loadApps) 365 + if upgraded == 0 && failed == 0 { 366 + return "Everything is up to date", nil 367 + } 368 + return fmt.Sprintf("%d app%s upgraded, %d upgrade%s failed", upgraded, plural(upgraded), failed, plural(failed)), nil 369 + }) 370 + } 371 + 372 + func (w *mainWindow) launchApp(info appherder.AppInfo) { 373 + w.run("Launching "+info.Name, func() (string, error) { 374 + if err := w.app.Launch(appKey(info)); err != nil { 375 + return "", err 376 + } 377 + return "", nil 378 + }) 379 + } 380 + 381 + func (w *mainWindow) promptInstallFile() { 382 + dialog := gtk.NewFileDialog() 383 + dialog.SetTitle("Install AppImage") 384 + 385 + filter := gtk.NewFileFilter() 386 + filter.SetName("AppImages") 387 + filter.AddSuffix("appimage") 388 + filter.AddSuffix("AppImage") 389 + 390 + allFiles := gtk.NewFileFilter() 391 + allFiles.SetName("All Files") 392 + allFiles.AddPattern("*") 393 + 394 + filters := gio.NewListStore(gtk.GTypeFileFilter) 395 + filters.Append(filter.Object) 396 + filters.Append(allFiles.Object) 397 + dialog.SetFilters(filters) 398 + dialog.SetDefaultFilter(filter) 399 + 400 + dialog.Open(context.Background(), &w.Window, func(result gio.AsyncResulter) { 401 + file, err := dialog.OpenFinish(result) 402 + if err != nil || file == nil { 403 + return 404 + } 405 + path := file.Path() 406 + if path == "" { 407 + w.showError("Cannot Install File", "Selected file has no local path.") 408 + return 409 + } 410 + w.install(path) 411 + }) 412 + } 413 + 414 + func (w *mainWindow) promptInstallURL() { 415 + entry := gtk.NewEntry() 416 + entry.SetPlaceholderText("https://example.com/app.AppImage") 417 + entry.SetHExpand(true) 418 + 419 + dialog := adw.NewAlertDialog("Install from URL", "Enter an HTTP or HTTPS AppImage URL.") 420 + dialog.SetExtraChild(entry) 421 + dialog.AddResponse("cancel", "Cancel") 422 + dialog.AddResponse("install", "Install") 423 + dialog.SetCloseResponse("cancel") 424 + dialog.SetDefaultResponse("install") 425 + dialog.ConnectResponse(func(response string) { 426 + if response != "install" { 427 + return 428 + } 429 + target := strings.TrimSpace(entry.Text()) 430 + if target == "" { 431 + w.showError("Cannot Install URL", "Enter a URL to install.") 432 + return 433 + } 434 + if !looksLikeURL(target) { 435 + w.showError("Cannot Install URL", "Enter an HTTP or HTTPS URL.") 436 + return 437 + } 438 + w.install(target) 439 + }) 440 + dialog.Present(w) 441 + } 442 + 443 + func (w *mainWindow) install(target string) { 444 + w.run("Installing AppImage", func() (string, error) { 445 + var name string 446 + var err error 447 + if looksLikeURL(target) { 448 + name, err = w.app.InstallFromURL(context.Background(), target) 449 + } else { 450 + name, err = w.app.Install(target) 451 + } 452 + if err != nil { 453 + return "", err 454 + } 455 + w.idle(w.loadApps) 456 + return fmt.Sprintf("Installed %s", name), nil 457 + }) 458 + } 459 + 460 + func (w *mainWindow) confirmUninstall(info appherder.AppInfo) { 461 + dialog := adw.NewAlertDialog("Remove "+info.Name+"?", "This removes the AppImage, launcher, icon, and appherder metadata.") 462 + dialog.AddResponse("cancel", "Cancel") 463 + dialog.AddResponse("remove", "Remove") 464 + dialog.SetCloseResponse("cancel") 465 + dialog.SetDefaultResponse("cancel") 466 + dialog.SetResponseAppearance("remove", adw.ResponseDestructive) 467 + dialog.ConnectResponse(func(response string) { 468 + if response == "remove" { 469 + w.uninstall(info) 470 + } 471 + }) 472 + dialog.Present(w) 473 + } 474 + 475 + func (w *mainWindow) uninstall(info appherder.AppInfo) { 476 + w.run("Removing "+info.Name, func() (string, error) { 477 + if err := w.app.Uninstall(appKey(info), false); err != nil { 478 + return "", err 479 + } 480 + w.idle(w.loadApps) 481 + return "Removed " + info.Name, nil 482 + }) 483 + } 484 + 485 + func (w *mainWindow) run(_ string, fn func() (string, error)) { 486 + w.setBusy(1) 487 + go func() { 488 + _, err := fn() 489 + w.idle(func() { 490 + w.setBusy(-1) 491 + if err != nil { 492 + w.showError("Operation Failed", err.Error()) 493 + return 494 + } 495 + }) 496 + }() 497 + } 498 + 499 + func (w *mainWindow) setBusy(delta int) { 500 + w.busy += delta 501 + if w.busy < 0 { 502 + w.busy = 0 503 + } 504 + sensitive := w.busy == 0 505 + w.installBtn.SetSensitive(sensitive) 506 + w.menuBtn.SetSensitive(sensitive) 507 + } 508 + 509 + func (w *mainWindow) idle(fn func()) { 510 + glib.IdleAdd(fn) 511 + } 512 + 513 + func (w *mainWindow) showError(title, message string) { 514 + dialog := adw.NewAlertDialog(title, message) 515 + dialog.AddResponse("close", "Close") 516 + dialog.SetCloseResponse("close") 517 + dialog.SetDefaultResponse("close") 518 + dialog.Present(w) 519 + } 520 + 521 + func looksLikeURL(s string) bool { 522 + parsed, err := url.Parse(s) 523 + return err == nil && (parsed.Scheme == "http" || parsed.Scheme == "https") && parsed.Host != "" 524 + } 525 + 526 + func summarizeApplied(applied []appherder.UpgradeApplied) (upgraded, failed int) { 527 + for _, app := range applied { 528 + if app.Err != nil { 529 + failed++ 530 + } else { 531 + upgraded++ 532 + } 533 + } 534 + return upgraded, failed 535 + } 536 + 537 + func plural(n int) string { 538 + if n == 1 { 539 + return "" 540 + } 541 + return "s" 542 + } 543 + 544 + func orDash(s string) string { 545 + if s == "" { 546 + return "-" 547 + } 548 + return s 549 + } 550 + 551 + func humanSize(bytes int64) string { 552 + const unit = 1024 553 + if bytes < unit { 554 + return fmt.Sprintf("%d B", bytes) 555 + } 556 + div, exp := int64(unit), 0 557 + for scaled := bytes / unit; scaled >= unit; scaled /= unit { 558 + div *= unit 559 + exp++ 560 + } 561 + return fmt.Sprintf("%.1f %cB", float64(bytes)/float64(div), "KMGTPE"[exp]) 562 + } 563 + 564 + func appKey(info appherder.AppInfo) string { 565 + if info.AppID != "" { 566 + return info.AppID 567 + } 568 + if info.Filename == "" { 569 + return appherder.NormalizeAppName(info.Name) 570 + } 571 + return strings.TrimSuffix(filepath.Base(info.Filename), filepath.Ext(info.Filename)) 572 + } 573 + 574 + func sizeOrDash(bytes int64) string { 575 + if bytes <= 0 { 576 + return "-" 577 + } 578 + return humanSize(bytes) 579 + } 580 + 581 + func sourceLabel(source string) string { 582 + switch source { 583 + case "", "none": 584 + return "No update source" 585 + case "github": 586 + return "GitHub" 587 + case "gitlab": 588 + return "GitLab" 589 + case "zsync": 590 + return "zsync" 591 + case "static": 592 + return "Static URL" 593 + default: 594 + return source 595 + } 596 + } 597 + 598 + func signatureLabel(signature string) string { 599 + switch signature { 600 + case "pinned": 601 + return "Pinned signature" 602 + case "signed": 603 + return "Signed" 604 + case "", "none": 605 + return "Unsigned" 606 + default: 607 + return signature 608 + } 609 + }
+20 -3
flake.nix
··· 33 33 packages = with pkgs; [ 34 34 go 35 35 dwarfs 36 + pkg-config 37 + glib 38 + gobject-introspection 39 + gtk4 40 + libadwaita 36 41 self.formatter.${system} 37 42 ]; 43 + 44 + shellHook = '' 45 + export GOCACHE="''${XDG_CACHE_HOME:-$HOME/.cache}/appherder/go-build" 46 + mkdir -p "$GOCACHE" 47 + ''; 38 48 }; 39 49 } 40 50 ); ··· 42 52 formatter = forEachSupportedSystem ({pkgs, ...}: pkgs.alejandra); 43 53 44 54 packages = forEachSupportedSystem ( 45 - {pkgs, ...}: { 46 - default = pkgs.callPackage ./package.nix {}; 47 - } 55 + {pkgs, ...}: let 56 + appherder = pkgs.callPackage ./package.nix {}; 57 + in 58 + { 59 + default = appherder; 60 + inherit appherder; 61 + } 62 + // lib.optionalAttrs pkgs.stdenv.isLinux { 63 + appherder-gui = pkgs.callPackage ./package-gui.nix {}; 64 + } 48 65 ); 49 66 }; 50 67 }
+5
go.mod
··· 7 7 github.com/ProtonMail/go-crypto v1.4.1 8 8 github.com/adrg/xdg v0.5.3 9 9 github.com/alyraffauf/goxdgdesktop v0.1.0 10 + github.com/diamondburned/gotk4-adwaita/pkg v0.0.0-20250703085337-e94555b846b6 11 + github.com/diamondburned/gotk4/pkg v0.3.2-0.20250703063411-16654385f59a 10 12 github.com/pelletier/go-toml/v2 v2.2.4 11 13 github.com/spf13/cobra v1.10.2 12 14 ) 13 15 14 16 require ( 17 + github.com/KarpelesLab/weak v0.1.1 // indirect 15 18 github.com/cloudflare/circl v1.6.3 // indirect 16 19 github.com/inconshreveable/mousetrap v1.1.0 // indirect 17 20 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect ··· 21 24 github.com/rasky/go-lzo v0.0.0-20200203143853-96a758eda86e // indirect 22 25 github.com/spf13/pflag v1.0.10 // indirect 23 26 github.com/ulikunitz/xz v0.5.15 // indirect 27 + go4.org/unsafe/assume-no-moving-gc v0.0.0-20231121144256-b99613f794b6 // indirect 24 28 golang.org/x/crypto v0.53.0 // indirect 29 + golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect 25 30 golang.org/x/sys v0.46.0 // indirect 26 31 )
+10
go.sum
··· 1 1 github.com/CalebQ42/squashfs v1.4.1 h1:tBcFMQSRQvWcY50e9r9cv2uVzNf06fcUhly0LeZg8bI= 2 2 github.com/CalebQ42/squashfs v1.4.1/go.mod h1:/As5wg6ScFFaab9SaNFNHyCOsd73Q5IFPOFJCVnwWzQ= 3 + github.com/KarpelesLab/weak v0.1.1 h1:fNnlPo3aypS9tBzoEQluY13XyUfd/eWaSE/vMvo9s4g= 4 + github.com/KarpelesLab/weak v0.1.1/go.mod h1:pzXsWs5f2bf+fpgHayTlBE1qJpO3MpJKo5sRaLu1XNw= 3 5 github.com/ProtonMail/go-crypto v1.4.1 h1:9RfcZHqEQUvP8RzecWEUafnZVtEvrBVL9BiF67IQOfM= 4 6 github.com/ProtonMail/go-crypto v1.4.1/go.mod h1:e1OaTyu5SYVrO9gKOEhTc+5UcXtTUa+P3uLudwcgPqo= 5 7 github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78= ··· 11 13 github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 12 14 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 13 15 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 16 + github.com/diamondburned/gotk4-adwaita/pkg v0.0.0-20250703085337-e94555b846b6 h1:WzOC3KtvrC1hJMz3fbJBg0Ye50nt4Tafor+a/bBHNEA= 17 + github.com/diamondburned/gotk4-adwaita/pkg v0.0.0-20250703085337-e94555b846b6/go.mod h1:ZzYiyPe0TqsukfPHi0sK/WwKzm0wIJdSRylLnuvAZNw= 18 + github.com/diamondburned/gotk4/pkg v0.3.2-0.20250703063411-16654385f59a h1:dN2jYYZ71hFhoKFSn24pQdKWLZb/XDydBt8pEIkFjJo= 19 + github.com/diamondburned/gotk4/pkg v0.3.2-0.20250703063411-16654385f59a/go.mod h1:O9K8+PGNFGJpAu8+u5D2Sn5Wae4hxWzHB+AeZNbV/2Q= 14 20 github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 15 21 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 16 22 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= ··· 38 44 github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY= 39 45 github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= 40 46 go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= 47 + go4.org/unsafe/assume-no-moving-gc v0.0.0-20231121144256-b99613f794b6 h1:lGdhQUN/cnWdSH3291CUuxSEqc+AsGTiDxPP3r2J0l4= 48 + go4.org/unsafe/assume-no-moving-gc v0.0.0-20231121144256-b99613f794b6/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E= 41 49 golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= 42 50 golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= 51 + golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= 52 + golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 43 53 golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= 44 54 golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= 45 55 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+25
justfile
··· 1 + set dotenv-load 2 + 3 + GOFLAGS := "-trimpath" 4 + GOCACHE := `printf '%s/appherder/go-build' "${XDG_CACHE_HOME:-$HOME/.cache}"` 5 + 6 + default: 7 + just --list 8 + 9 + build: build-cli build-ui 10 + 11 + build-cli: 12 + mkdir -p '{{GOCACHE}}' 13 + nix develop -c env GOCACHE='{{GOCACHE}}' GOSUMDB=off go build {{GOFLAGS}} -o ./appherder ./cmd/appherder 14 + 15 + build-ui: 16 + mkdir -p '{{GOCACHE}}' 17 + nix develop -c env GOCACHE='{{GOCACHE}}' GOSUMDB=off go build {{GOFLAGS}} -tags gtk -o ./appherder-gui ./cmd/appherder-gui 18 + 19 + test: 20 + mkdir -p '{{GOCACHE}}' 21 + nix develop -c env GOCACHE='{{GOCACHE}}' GOSUMDB=off go test ./... 22 + 23 + test-ui: 24 + mkdir -p '{{GOCACHE}}' 25 + nix develop -c env GOCACHE='{{GOCACHE}}' GOSUMDB=off go test -tags gtk ./cmd/appherder-gui
+38
package-gui.nix
··· 1 + { 2 + buildGoModule, 3 + glib, 4 + gtk4, 5 + lib, 6 + libadwaita, 7 + pkg-config, 8 + wrapGAppsHook4, 9 + }: let 10 + version = "dev"; 11 + in 12 + buildGoModule { 13 + pname = "appherder-gui"; 14 + inherit version; 15 + src = ./.; 16 + vendorHash = "sha256-YoNtqb5dflJNCZBstAQxP458ktpUighC8uYuqFWjTyo="; 17 + subPackages = ["cmd/appherder-gui"]; 18 + tags = ["gtk"]; 19 + ldflags = ["-X main.version=${version}"]; 20 + 21 + nativeBuildInputs = [ 22 + pkg-config 23 + wrapGAppsHook4 24 + ]; 25 + 26 + buildInputs = [ 27 + glib 28 + gtk4 29 + libadwaita 30 + ]; 31 + 32 + meta = { 33 + description = "GTK/libadwaita interface for AppHerder"; 34 + license = lib.licenses.gpl3Only; 35 + mainProgram = "appherder-gui"; 36 + platforms = lib.platforms.linux; 37 + }; 38 + }