powerful but friendly backup program that runs in your tray, powered by restic devins.page/restray
go restic system-tray
2

Configure Feed

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

fix: keep schedules active if env files unavailable

mainly was a problem for those declaratively managing configuration with home-manager (including myself) where the agenix env files didn't exist yet, locking up the entire program...

intergrav (Jul 13, 2026, 7:39 PM EDT) 1fa42608 ac4cf7bd

+24 -14
+4 -1
cmd/restray/cli.go
··· 281 281 } 282 282 283 283 func cliSchedule(prof Profile) error { 284 + if errMsg := prof.profileError(); errMsg != "" { 285 + return cli.Exit("error: "+errMsg, 1) 286 + } 284 287 p, _ := findBackend(prof) 285 288 if p == "" { 286 289 return cli.Exit("error: "+prof.backendName()+" not found", 1) ··· 365 368 } 366 369 key := prof.profileKey() 367 370 name := prof.displayName() 368 - if errMsg := prof.scheduleError(); errMsg != "" { 371 + if errMsg := prof.scheduleConfigError(); errMsg != "" { 369 372 log.Printf("[%s] skipping schedule: %s", name, errMsg) 370 373 continue 371 374 }
+12 -11
cmd/restray/config.go
··· 133 133 return env, nil 134 134 } 135 135 136 - func (prof Profile) scheduleError() string { 137 - vars, err := parseEnvFile(prof.EnvFile) 138 - if err != nil { 139 - return "Env file not found" 140 - } 136 + func (prof Profile) scheduleConfigError() string { 141 137 if prof.backend() == "" { 142 138 return "Unsupported backend: " + prof.Backend 143 139 } 144 - if err := backendEnvError(prof.backend(), vars); err != "" { 145 - return err 146 - } 147 140 if prof.Schedule.BackupEnabled() && len(prof.Backup.Paths) == 0 { 148 141 return "No paths configured" 149 142 } 150 143 return "" 151 144 } 152 145 153 - func (prof Profile) configError() string { 154 - if err := prof.scheduleError(); err != "" { 146 + func (prof Profile) environmentError() string { 147 + vars, err := parseEnvFile(prof.EnvFile) 148 + if err != nil { 149 + return "Env file not available" 150 + } 151 + return backendEnvError(prof.backend(), vars) 152 + } 153 + 154 + func (prof Profile) profileError() string { 155 + if err := prof.scheduleConfigError(); err != "" { 155 156 return err 156 157 } 157 158 if len(prof.Backup.Paths) == 0 { 158 159 return "No paths configured" 159 160 } 160 - return "" 161 + return prof.environmentError() 161 162 } 162 163 163 164 func (prof Profile) displayName() string {
+6
cmd/restray/operations.go
··· 354 354 setProfileFailed(key, "") 355 355 defer onDone() 356 356 defer releaseProfile(key) 357 + if errMsg := prof.profileError(); errMsg != "" { 358 + mStatus.SetTitle(errMsg) 359 + setProfileFailed(key, errMsg) 360 + notifyError("Schedule", errMsg) 361 + return 362 + } 357 363 358 364 hookEnv := []string{ 359 365 "RESTRAY_OPERATIONS=" + strings.Join(scheduledOpNames(prof), ","),
+2 -2
cmd/restray/tray.go
··· 531 531 ps.errMsg = prof.backendDisplayName() + " not found" 532 532 return ps 533 533 } 534 - ps.errMsg = prof.configError() 534 + ps.errMsg = prof.profileError() 535 535 if ps.errMsg == "" { 536 536 wantsLast := cfg.GUI.ScheduleDisplay == "last" && getLastBackup(prof.profileKey()).IsZero() 537 537 var rs repoResult ··· 704 704 ps := probeProfileState(cfg, i, prof) 705 705 706 706 if prof.Schedule.Cron != "" && i < maxProfiles { 707 - if errMsg := prof.scheduleError(); errMsg != "" { 707 + if errMsg := prof.scheduleConfigError(); errMsg != "" { 708 708 log.Printf("[%s] skipping schedule: %s", prof.displayName(), errMsg) 709 709 } else { 710 710 key := prof.profileKey()