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.

feat: support self-manage on linux

now self-managed restic is supported on windows and linux as a fallback if not found in PATH. will remain unsupported on darwin due to macos security killing the restic process randomly

intergrav (Jul 2, 2026, 7:42 AM EDT) 136231ad e7e4a54a

+50 -35
+3 -3
README.md
··· 19 19 - Optional automatic pruning and checks after backups 20 20 - Operations menu for running individual operations (backup, prune, check, unlock, mount, shell, hooks) 21 21 - Optional automatic retry and removal of stale locks 22 - - On Windows it can also download and update it's own self-managed restic if not found in PATH 22 + - On Windows/Linux it can also download and update it's own self-managed restic if not found in PATH 23 23 - Open a shell with your profile's environment and restic binary loaded, for restoring or other CLI operations without friction 24 24 - Mount repository snapshots on macOS/Linux 25 25 - Requires [FUSE-T](https://www.fuse-t.org) or [macFUSE](https://macfuse.github.io) on macOS. Requires [FUSE](https://github.com/libfuse/libfuse) on Linux ··· 34 34 35 35 To install, grab a build artifact for your OS and architecture from the [repository's tags](https://tangled.org/devins.page/restray/tags). On MacOS you'll need to [remove quarantine](https://disable-gatekeeper.github.io#disabling-gatekeeper-for-one-application-only) from the `.app`, since I can't pay Apple $100/year to sign it. Sorry. 36 36 37 - On MacOS, a restic binary is downloaded and bundled to the `.app` at build time, though I'd recommend installing restic to your PATH so you have the latest version. On Windows, Restray can download and update it's own restic binary itself if it isn't already in PATH. On Linux, you should install restic with your package manager. 37 + On MacOS, a restic binary is downloaded and bundled to the `.app` at build time, though I'd recommend installing restic to your PATH so you have the latest version. On Windows and Linux, Restray can download and update it's own restic binary itself if it isn't already in PATH. 38 38 39 39 _If you're a Nix/NixOS/Nix-darwin user, I also provide package/module in the project's [flake](https://tangled.org/devins.page/restray) for Linux and MacOS._ 40 40 ··· 59 59 | Key | Type | Default | Description | 60 60 | ------------------ | ------ | --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 61 61 | `check_updates` | bool | `true` | **MacOS/Windows only.** Check for new Restray versions on startup and periodically. When an update is found, the version item in the menu changes to an update button | 62 - | `manage_restic` | bool | `false` | **Windows only.** If restic is not found in PATH, automatically downloads and updates a self-managed restic binary daily from GitHub. | 62 + | `manage_restic` | bool | `false` | **Windows/Linux only.** If restic is not found in PATH, automatically downloads and updates a self-managed restic binary daily from GitHub. | 63 63 | `notifications` | string | `"none"` | `"all"` (success + errors), `"errors"` (errors only), or `"none"` (silent) | 64 64 | `icon` | string | platform | `"color"`, `"mono"`, `"white"`, or `"black"`. Default is `"mono"` on MacOS/Windows and `"color"` elsewhere. `"mono"` follows OS light/dark mode. | 65 65 | `schedule_display` | string | `"description"` | `"description"` (human-readable, e.g. "every 6 hours"), `"cron"` (raw expression, e.g. "0 \* \* \* \*"), `"last"` (time since last backup, e.g. "2 hours ago"), or `"none"` |
+1 -2
cmd/restray/manage_other.go
··· 1 - //go:build !windows 1 + //go:build !windows && !linux 2 2 3 3 package main 4 4 5 5 import "fyne.io/systray" 6 6 7 - // macOS/Linux require system restic, the app never downloads or manages one 8 7 const selfManagesRestic = false 9 8 10 9 func downloadRestic(prefixedMenuItem) error { return nil }
+45 -29
cmd/restray/manage_windows.go cmd/restray/manage_windows_linux.go
··· 1 - //go:build windows 1 + //go:build windows || linux 2 2 3 3 package main 4 4 5 5 import ( 6 6 "archive/zip" 7 + "compress/bzip2" 7 8 "fmt" 8 9 "io" 9 10 "log" ··· 23 24 24 25 var downloading sync.Mutex 25 26 27 + func resticBinaryName() string { 28 + if runtime.GOOS == "windows" { 29 + return "restic.exe" 30 + } 31 + return "restic" 32 + } 33 + 26 34 func managedRestic() string { 27 - p := filepath.Join(dataDir(), "restic.exe") 35 + p := filepath.Join(dataDir(), resticBinaryName()) 28 36 if _, err := os.Stat(p); err == nil { 29 37 return p 30 38 } ··· 92 100 return fmt.Errorf("failed to determine latest version") 93 101 } 94 102 95 - url := fmt.Sprintf("https://github.com/restic/restic/releases/download/v%s/restic_%s_%s_%s.zip", 96 - version, version, runtime.GOOS, runtime.GOARCH) 103 + ext := "bz2" 104 + if runtime.GOOS == "windows" { 105 + ext = "zip" 106 + } 107 + url := fmt.Sprintf("https://github.com/restic/restic/releases/download/v%s/restic_%s_%s_%s.%s", 108 + version, version, runtime.GOOS, runtime.GOARCH, ext) 97 109 log.Printf("downloading restic %s", version) 98 110 mStatus.SetTitle("Downloading restic " + version + "...") 99 111 ··· 108 120 return fmt.Errorf("download failed: %s", resp.Status) 109 121 } 110 122 111 - if err = extractResticZip(resp.Body); err == nil { 123 + if err = extractRestic(resp.Body); err == nil { 112 124 log.Printf("installed restic %s", version) 113 125 resetResticPath() 114 126 } 115 127 return err 116 128 } 117 129 118 - func installBinary(r io.Reader) error { 119 - p := filepath.Join(dataDir(), "restic.exe") 120 - tmp, err := os.CreateTemp(filepath.Dir(p), "restic-download-*") 121 - if err != nil { 122 - return err 123 - } 124 - tmpName := tmp.Name() 125 - if _, err := io.Copy(tmp, r); err != nil { 126 - tmp.Close() 127 - os.Remove(tmpName) 128 - return err 129 - } 130 - tmp.Close() 131 - if err := os.Chmod(tmpName, 0755); err != nil { 132 - os.Remove(tmpName) 133 - return err 134 - } 135 - os.Remove(p) 136 - if err := os.Rename(tmpName, p); err != nil { 137 - os.Remove(tmpName) 138 - return err 130 + func extractRestic(r io.Reader) error { 131 + if runtime.GOOS != "windows" { 132 + return installBinary(bzip2.NewReader(r)) 139 133 } 140 - return nil 141 - } 142 134 143 - func extractResticZip(r io.Reader) error { 144 135 tmp, err := os.CreateTemp("", "restray-download-*.zip") 145 136 if err != nil { 146 137 return err ··· 169 160 } 170 161 } 171 162 return fmt.Errorf("restic binary not found in zip") 163 + } 164 + 165 + func installBinary(r io.Reader) error { 166 + p := filepath.Join(dataDir(), resticBinaryName()) 167 + tmp, err := os.CreateTemp(filepath.Dir(p), "restic-download-*") 168 + if err != nil { 169 + return err 170 + } 171 + tmpName := tmp.Name() 172 + if _, err := io.Copy(tmp, r); err != nil { 173 + tmp.Close() 174 + os.Remove(tmpName) 175 + return err 176 + } 177 + tmp.Close() 178 + if err := os.Chmod(tmpName, 0755); err != nil { 179 + os.Remove(tmpName) 180 + return err 181 + } 182 + os.Remove(p) 183 + if err := os.Rename(tmpName, p); err != nil { 184 + os.Remove(tmpName) 185 + return err 186 + } 187 + return nil 172 188 } 173 189 174 190 func checkResticUpdate(resticPath string, autoUpdate bool, mUpdate *systray.MenuItem, mStatus prefixedMenuItem, onUpdated func()) {
+1 -1
nix/shared.nix
··· 12 12 manageRestic = lib.mkOption { 13 13 type = lib.types.bool; 14 14 default = false; 15 - description = "Windows only. If restic is not found in PATH, automatically downloads and updates a self-managed restic binary daily from GitHub."; 15 + description = "Windows/Linux only. If restic is not found in PATH, automatically downloads and updates a self-managed restic binary daily from GitHub."; 16 16 }; 17 17 notifications = lib.mkOption { 18 18 type = lib.types.str;