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.

refactor: restructure, build constraints, cleanup

intergrav (Jun 29, 2026, 6:36 AM EDT) a34c705d 6389ce10

+772 -702
+2 -2
.gitignore
··· 3 3 bin/ 4 4 dist/ 5 5 result 6 - Restray 7 - Restray.exe 6 + /Restray 7 + /Restray.exe 8 8 restray_windows_*.syso
+261
cmd/restray/config.go
··· 1 + package main 2 + 3 + import ( 4 + "bufio" 5 + _ "embed" 6 + "log" 7 + "os" 8 + "path/filepath" 9 + "runtime" 10 + "strings" 11 + "time" 12 + 13 + "github.com/BurntSushi/toml" 14 + "github.com/fsnotify/fsnotify" 15 + ) 16 + 17 + //go:embed templates/config.toml 18 + var defaultConfigTemplate string 19 + 20 + //go:embed templates/default.env 21 + var defaultEnvTemplate string 22 + 23 + type Schedule struct { 24 + Cron string `toml:"cron"` 25 + OnBattery bool `toml:"on_battery"` 26 + Backup *bool `toml:"backup"` 27 + Prune bool `toml:"prune"` 28 + Check bool `toml:"check"` 29 + } 30 + 31 + func (s Schedule) BackupEnabled() bool { 32 + return s.Backup == nil || *s.Backup 33 + } 34 + 35 + type Backup struct { 36 + Paths []string `toml:"paths"` 37 + Args []string `toml:"args"` 38 + ArgsScheduled []string `toml:"args_scheduled"` 39 + } 40 + 41 + type Prune struct { 42 + Args []string `toml:"args"` 43 + } 44 + 45 + type Check struct { 46 + Args []string `toml:"args"` 47 + } 48 + 49 + type Mount struct { 50 + Args []string `toml:"args"` 51 + } 52 + 53 + type Profile struct { 54 + Name string `toml:"name"` 55 + EnvFile string `toml:"env_file"` 56 + RetryLock string `toml:"retry_lock"` 57 + PreHook string `toml:"pre_hook"` 58 + PostHook string `toml:"post_hook"` 59 + Schedule Schedule `toml:"schedule"` 60 + Backup Backup `toml:"backup"` 61 + Prune Prune `toml:"prune"` 62 + Check Check `toml:"check"` 63 + Mount Mount `toml:"mount"` 64 + } 65 + 66 + type General struct { 67 + ManageRestic bool `toml:"manage_restic"` 68 + Notifications string `toml:"notifications"` 69 + ScheduleDisplay string `toml:"schedule_display"` 70 + Terminal string `toml:"terminal"` 71 + } 72 + 73 + type Config struct { 74 + General General `toml:"general"` 75 + Profile []Profile `toml:"profile"` 76 + } 77 + 78 + func parseEnvFile(path string) (map[string]string, error) { 79 + f, err := os.Open(path) 80 + if err != nil { 81 + return nil, err 82 + } 83 + defer f.Close() 84 + 85 + env := make(map[string]string) 86 + scanner := bufio.NewScanner(f) 87 + for scanner.Scan() { 88 + line := strings.TrimSpace(scanner.Text()) 89 + if line == "" || strings.HasPrefix(line, "#") { 90 + continue 91 + } 92 + line = strings.TrimPrefix(line, "export ") 93 + key, val, ok := strings.Cut(line, "=") 94 + if !ok { 95 + continue 96 + } 97 + key = strings.TrimSpace(key) 98 + val = strings.TrimSpace(val) 99 + if len(val) >= 2 && (val[0] == '"' || val[0] == '\'') && val[len(val)-1] == val[0] { 100 + val = val[1 : len(val)-1] 101 + } 102 + env[key] = val 103 + } 104 + return env, nil 105 + } 106 + 107 + func (prof Profile) configError() string { 108 + vars, err := parseEnvFile(prof.EnvFile) 109 + if err != nil { 110 + return "Env file not found" 111 + } 112 + if vars["RESTIC_REPOSITORY"] == "" { 113 + return "RESTIC_REPOSITORY not set in env file" 114 + } 115 + if vars["RESTIC_PASSWORD"] == "" && vars["RESTIC_PASSWORD_FILE"] == "" && vars["RESTIC_PASSWORD_COMMAND"] == "" { 116 + return "No password set in env file" 117 + } 118 + if len(prof.Backup.Paths) == 0 { 119 + return "No paths configured" 120 + } 121 + return "" 122 + } 123 + 124 + func (prof Profile) displayName() string { 125 + if prof.Name != "" { 126 + return prof.Name 127 + } 128 + if prof.EnvFile != "" { 129 + return strings.TrimSuffix(filepath.Base(prof.EnvFile), ".env") 130 + } 131 + return "Profile" 132 + } 133 + 134 + func envFilesInsecure(profiles []Profile) bool { 135 + if runtime.GOOS == "windows" { 136 + return false 137 + } 138 + for _, p := range profiles { 139 + info, err := os.Stat(p.EnvFile) 140 + if err == nil && info.Mode().Perm()&0077 != 0 { 141 + return true 142 + } 143 + } 144 + return false 145 + } 146 + 147 + func fixEnvFilePermissions(profiles []Profile) { 148 + for _, p := range profiles { 149 + os.Chmod(p.EnvFile, 0600) 150 + } 151 + } 152 + 153 + func configDir() string { 154 + dir, _ := os.UserConfigDir() 155 + return filepath.Join(dir, "restray") 156 + } 157 + 158 + func configPath() string { return filepath.Join(configDir(), "config.toml") } 159 + func defaultEnvPath(name string) string { 160 + if name == "" { 161 + name = "default" 162 + } 163 + return filepath.Join(configDir(), name+".env") 164 + } 165 + 166 + func logPath() string { return filepath.Join(configDir(), "restray.log") } 167 + func lockPath() string { return filepath.Join(configDir(), "restray.lock") } 168 + 169 + func loadConfig() Config { 170 + cfg := Config{} 171 + if _, err := toml.DecodeFile(configPath(), &cfg); err != nil && !os.IsNotExist(err) { 172 + log.Printf("config: %v", err) 173 + } 174 + if cfg.General.ScheduleDisplay == "" { 175 + cfg.General.ScheduleDisplay = "description" 176 + } 177 + if len(cfg.Profile) == 0 { 178 + cfg.Profile = append(cfg.Profile, Profile{Name: "default"}) 179 + } 180 + for i := range cfg.Profile { 181 + prof := &cfg.Profile[i] 182 + if prof.EnvFile == "" { 183 + prof.EnvFile = defaultEnvPath(prof.Name) 184 + } 185 + if prof.RetryLock == "" { 186 + prof.RetryLock = "2m" 187 + } 188 + if prof.Backup.Args == nil { 189 + prof.Backup.Args = []string{"--exclude-caches", "--exclude", "*.tmp"} 190 + } 191 + if prof.Backup.ArgsScheduled == nil { 192 + prof.Backup.ArgsScheduled = []string{"--tag", "scheduled"} 193 + } 194 + if prof.Prune.Args == nil { 195 + prof.Prune.Args = []string{ 196 + "--keep-last", "4", 197 + "--keep-hourly", "24", 198 + "--keep-daily", "7", 199 + "--keep-weekly", "4", 200 + "--keep-monthly", "12", 201 + } 202 + } 203 + } 204 + return cfg 205 + } 206 + 207 + func defaultConfigContents() string { 208 + return strings.ReplaceAll(defaultConfigTemplate, "{{ENV_FILE}}", filepath.ToSlash(defaultEnvPath("default"))) 209 + } 210 + 211 + func ensureConfigFile() { 212 + p := configPath() 213 + if _, err := os.Stat(p); err == nil { 214 + return 215 + } 216 + os.MkdirAll(filepath.Dir(p), 0700) 217 + os.WriteFile(p, []byte(defaultConfigContents()), 0600) 218 + } 219 + 220 + func ensureEnvFile(path string) { 221 + if _, err := os.Stat(path); err == nil { 222 + return 223 + } 224 + os.MkdirAll(filepath.Dir(path), 0700) 225 + os.WriteFile(path, []byte(defaultEnvTemplate), 0600) 226 + } 227 + 228 + func fileExists(path string) bool { 229 + _, err := os.Stat(path) 230 + return err == nil 231 + } 232 + 233 + func watchConfig(onChange func()) { 234 + watcher, err := fsnotify.NewWatcher() 235 + if err != nil { 236 + return 237 + } 238 + watcher.Add(configDir()) 239 + 240 + go func() { 241 + for range watcher.Errors { 242 + } 243 + }() 244 + go func() { 245 + var debounce *time.Timer 246 + for event := range watcher.Events { 247 + ext := filepath.Ext(event.Name) 248 + if ext != ".toml" && ext != ".env" { 249 + continue 250 + } 251 + action := event.Has(fsnotify.Write) || event.Has(fsnotify.Create) || 252 + event.Has(fsnotify.Remove) || event.Has(fsnotify.Rename) 253 + if action { 254 + if debounce != nil { 255 + debounce.Stop() 256 + } 257 + debounce = time.AfterFunc(200*time.Millisecond, onChange) 258 + } 259 + } 260 + }() 261 + }
+64
cmd/restray/console_darwin.go
··· 1 + //go:build darwin 2 + 3 + package main 4 + 5 + import ( 6 + "fmt" 7 + "os" 8 + "os/exec" 9 + "path/filepath" 10 + "strings" 11 + ) 12 + 13 + func consoleCmd(vars map[string]string, consoleMsg, tailMsg, tailFile, terminal string) *exec.Cmd { 14 + script, err := os.CreateTemp("", "restray-shell-*.sh") 15 + if err != nil { 16 + return nil 17 + } 18 + shell := os.Getenv("SHELL") 19 + if shell == "" { 20 + shell = "/bin/zsh" 21 + } 22 + fmt.Fprintf(script, "#!/bin/sh\n") 23 + for k, v := range vars { 24 + fmt.Fprintf(script, "export %s=%q\n", k, v) 25 + } 26 + if p, _ := findRestic(); p != "" { 27 + fmt.Fprintf(script, "export PATH=%q:\"$PATH\"\n", filepath.Dir(p)) 28 + } 29 + fmt.Fprintf(script, "clear\n") 30 + fmt.Fprintf(script, "rm -f %q\n", script.Name()) 31 + fmt.Fprintf(script, "echo '%s'\n", consoleMsg) 32 + if tailFile != "" { 33 + fmt.Fprintf(script, "echo '%s'\n", tailMsg) 34 + fmt.Fprintf(script, "tail -n +1 -f %q &\n", tailFile) 35 + fmt.Fprintf(script, "TAIL_PID=$!\n") 36 + fmt.Fprintf(script, "trap 'kill $TAIL_PID 2>/dev/null; wait $TAIL_PID 2>/dev/null' INT\n") 37 + fmt.Fprintf(script, "wait $TAIL_PID\n") 38 + } 39 + fmt.Fprintf(script, "cd %q\n", configDir()) 40 + fmt.Fprintf(script, "exec %s -l\n", shell) 41 + script.Close() 42 + os.Chmod(script.Name(), 0700) 43 + return exec.Command("open", "-a", darwinTerminalApp(terminal), script.Name()) 44 + } 45 + 46 + func darwinTerminalApp(terminal string) string { 47 + t := strings.TrimSuffix(strings.ToLower(terminal), ".app") 48 + switch t { 49 + case "", "terminal": 50 + return "Terminal" 51 + case "iterm", "iterm2": 52 + return "iTerm" 53 + case "alacritty": 54 + return "Alacritty" 55 + case "kitty": 56 + return "kitty" 57 + case "ghostty": 58 + return "Ghostty" 59 + case "wezterm": 60 + return "WezTerm" 61 + default: 62 + return terminal 63 + } 64 + }
+50
cmd/restray/console_other.go
··· 1 + //go:build !darwin && !windows 2 + 3 + package main 4 + 5 + import ( 6 + "fmt" 7 + "log" 8 + "os" 9 + "os/exec" 10 + ) 11 + 12 + func findTerminal() string { 13 + if t := os.Getenv("TERMINAL"); t != "" { 14 + return t 15 + } 16 + for _, t := range []string{"x-terminal-emulator", "kgx", "gnome-terminal", "konsole", "xfce4-terminal", "alacritty", "kitty", "foot", "wezterm", "ghostty", "xterm"} { 17 + if p, err := exec.LookPath(t); err == nil { 18 + return p 19 + } 20 + } 21 + return "" 22 + } 23 + 24 + func consoleCmd(vars map[string]string, consoleMsg, tailMsg, tailFile, terminal string) *exec.Cmd { 25 + shell := os.Getenv("SHELL") 26 + if shell == "" { 27 + shell = "/bin/sh" 28 + } 29 + term := terminal 30 + if term == "" { 31 + term = findTerminal() 32 + } 33 + if term == "" { 34 + log.Print("console: no terminal emulator found") 35 + return nil 36 + } 37 + var script string 38 + if tailFile != "" { 39 + script = fmt.Sprintf("cd %q; clear; echo '%s'; echo '%s'; tail -n +1 -f %q; exec %s", configDir(), consoleMsg, tailMsg, tailFile, shell) 40 + } else { 41 + script = fmt.Sprintf("cd %q; clear; echo '%s'; exec %s", configDir(), consoleMsg, shell) 42 + } 43 + cmd := exec.Command(term, "-e", shell, "-c", script) 44 + env := os.Environ() 45 + for k, v := range vars { 46 + env = append(env, k+"="+v) 47 + } 48 + cmd.Env = env 49 + return cmd 50 + }
+36
cmd/restray/console_windows.go
··· 1 + //go:build windows 2 + 3 + package main 4 + 5 + import ( 6 + "fmt" 7 + "os" 8 + "os/exec" 9 + "path/filepath" 10 + ) 11 + 12 + func consoleCmd(vars map[string]string, consoleMsg, tailMsg, tailFile, _ string) *exec.Cmd { 13 + script, err := os.CreateTemp("", "restray-shell-*.bat") 14 + if err != nil { 15 + return nil 16 + } 17 + fmt.Fprintf(script, "@echo off\r\n") 18 + for k, v := range vars { 19 + fmt.Fprintf(script, "set %s=%s\r\n", k, v) 20 + } 21 + if p, _ := findRestic(); p != "" { 22 + fmt.Fprintf(script, "set PATH=%s;%%PATH%%\r\n", filepath.Dir(p)) 23 + } 24 + fmt.Fprintf(script, "echo %s\r\n", consoleMsg) 25 + if tailFile != "" { 26 + fmt.Fprintf(script, "echo %s\r\n", tailMsg) 27 + fmt.Fprintf(script, "echo.\r\n") 28 + fmt.Fprintf(script, "powershell -Command \"Get-Content -Path '%s' -Wait\"\r\n", tailFile) 29 + } 30 + fmt.Fprintf(script, "cd /d %q\r\n", configDir()) 31 + fmt.Fprintf(script, "cmd /k\r\n") 32 + script.Close() 33 + cmd := exec.Command("cmd", "/c", "start", "", script.Name()) 34 + showConsole(cmd) 35 + return cmd 36 + }
+29
cmd/restray/fda_darwin.go
··· 1 + //go:build darwin 2 + 3 + package main 4 + 5 + import ( 6 + "os" 7 + "os/exec" 8 + "strings" 9 + ) 10 + 11 + func needsFullDiskAccess() bool { 12 + f, err := os.Open("/Library/Application Support/com.apple.TCC/TCC.db") 13 + if err == nil { 14 + f.Close() 15 + } 16 + return os.IsPermission(err) 17 + } 18 + 19 + func openFullDiskAccessSettings() { 20 + exec.Command("open", "x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles").Start() 21 + } 22 + 23 + func promptFullDiskAccess() { 24 + out, err := exec.Command("osascript", "-e", 25 + `display dialog "Restray needs Full Disk Access to be able to back up all your files." buttons {"Skip", "Open Settings"} default button "Open Settings" with title "Restray" with icon caution`).Output() 26 + if err == nil && strings.Contains(string(out), "Open Settings") { 27 + openFullDiskAccessSettings() 28 + } 29 + }
+7
cmd/restray/fda_other.go
··· 1 + //go:build !darwin 2 + 3 + package main 4 + 5 + func needsFullDiskAccess() bool { return false } 6 + func openFullDiskAccessSettings() {} 7 + func promptFullDiskAccess() {}
+13
cmd/restray/open_darwin.go
··· 1 + //go:build darwin 2 + 3 + package main 4 + 5 + import "os/exec" 6 + 7 + func openFile(path string) { 8 + exec.Command("open", path).Start() 9 + } 10 + 11 + func openInEditor(path string) { 12 + exec.Command("open", "-t", path).Start() 13 + }
+13
cmd/restray/open_other.go
··· 1 + //go:build !darwin && !windows 2 + 3 + package main 4 + 5 + import "os/exec" 6 + 7 + func openFile(path string) { 8 + exec.Command("xdg-open", path).Start() 9 + } 10 + 11 + func openInEditor(path string) { 12 + openFile(path) 13 + }
+13
cmd/restray/open_windows.go
··· 1 + //go:build windows 2 + 3 + package main 4 + 5 + import "os/exec" 6 + 7 + func openFile(path string) { 8 + exec.Command("rundll32", "url.dll,FileProtocolHandler", path).Start() 9 + } 10 + 11 + func openInEditor(path string) { 12 + openFile(path) 13 + }
+238
cmd/restray/restic.go
··· 1 + package main 2 + 3 + import ( 4 + "context" 5 + "encoding/json" 6 + "log" 7 + "os" 8 + "os/exec" 9 + "path/filepath" 10 + "runtime" 11 + "strings" 12 + "sync" 13 + "time" 14 + ) 15 + 16 + // optional restic path baked at build time for Nix and other pkg managers 17 + var resticBuiltinPath string 18 + 19 + var ( 20 + resticOnce sync.Once 21 + resticPath string 22 + resticManaged bool 23 + ) 24 + 25 + func findRestic() (path string, managed bool) { 26 + resticOnce.Do(func() { 27 + if p, err := exec.LookPath("restic"); err == nil { 28 + resticPath = p 29 + return 30 + } 31 + if p := findResticFromShell(); p != "" { 32 + resticPath = p 33 + return 34 + } 35 + if resticBuiltinPath != "" { 36 + if _, err := os.Stat(resticBuiltinPath); err == nil { 37 + resticPath = resticBuiltinPath 38 + return 39 + } 40 + } 41 + if p := managedRestic(); p != "" { 42 + resticPath = p 43 + resticManaged = true 44 + } 45 + }) 46 + return resticPath, resticManaged 47 + } 48 + 49 + func findResticFromShell() string { 50 + if runtime.GOOS == "windows" { 51 + return "" 52 + } 53 + shell := os.Getenv("SHELL") 54 + if shell == "" { 55 + if runtime.GOOS == "darwin" { 56 + shell = "/bin/zsh" 57 + } else { 58 + shell = "/bin/sh" 59 + } 60 + } 61 + out, err := exec.Command(shell, "-l", "-c", "which restic").Output() 62 + if err == nil { 63 + if p := strings.TrimSpace(string(out)); p != "" { 64 + if _, err := os.Stat(p); err == nil { 65 + return p 66 + } 67 + } 68 + } 69 + for _, dir := range []string{ 70 + "/opt/homebrew/bin", 71 + "/usr/local/bin", 72 + filepath.Join(os.Getenv("HOME"), ".nix-profile/bin"), 73 + "/run/current-system/sw/bin", 74 + filepath.Join(os.Getenv("HOME"), ".local/bin"), 75 + } { 76 + p := filepath.Join(dir, "restic") 77 + if _, err := os.Stat(p); err == nil { 78 + return p 79 + } 80 + } 81 + return "" 82 + } 83 + 84 + func resticEnv(prof Profile) []string { 85 + env := os.Environ() 86 + vars, err := parseEnvFile(prof.EnvFile) 87 + if err != nil { 88 + return env 89 + } 90 + for k, v := range vars { 91 + env = append(env, k+"="+v) 92 + } 93 + return env 94 + } 95 + 96 + func resticCmd(prof Profile, args ...string) *exec.Cmd { 97 + path, _ := findRestic() 98 + log.Printf("[%s] run: restic %s", prof.displayName(), strings.Join(args, " ")) 99 + cmd := exec.Command(path, args...) 100 + cmd.Env = resticEnv(prof) 101 + hideWindow(cmd) 102 + return cmd 103 + } 104 + 105 + type repoResult struct { 106 + errMsg string 107 + needsInit bool 108 + } 109 + 110 + func repoStatus(prof Profile) repoResult { 111 + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 112 + defer cancel() 113 + path, _ := findRestic() 114 + cmd := exec.CommandContext(ctx, path, "cat", "config", "--no-lock", "-q") 115 + cmd.Env = resticEnv(prof) 116 + hideWindow(cmd) 117 + out, err := cmd.CombinedOutput() 118 + if err != nil { 119 + if ctx.Err() != nil { 120 + return repoResult{errMsg: "Repository unreachable"} 121 + } 122 + lines := strings.Split(strings.TrimSpace(string(out)), "\n") 123 + msg, code := classifyError(err, lines[len(lines)-1]) 124 + return repoResult{errMsg: msg, needsInit: code == 10} 125 + } 126 + return repoResult{} 127 + } 128 + 129 + type appState struct { 130 + mu sync.Mutex 131 + busyProfiles map[int]*exec.Cmd 132 + onBusyChanged func(bool) 133 + mountCmds map[int]*exec.Cmd 134 + mountStopping map[int]bool 135 + notifications string 136 + failStatus map[int]string 137 + } 138 + 139 + var state = appState{ 140 + busyProfiles: make(map[int]*exec.Cmd), 141 + mountCmds: make(map[int]*exec.Cmd), 142 + mountStopping: make(map[int]bool), 143 + failStatus: make(map[int]string), 144 + } 145 + 146 + func isProfileBusy(idx int) bool { 147 + state.mu.Lock() 148 + defer state.mu.Unlock() 149 + _, ok := state.busyProfiles[idx] 150 + return ok 151 + } 152 + 153 + func isAnyBusy() bool { 154 + state.mu.Lock() 155 + defer state.mu.Unlock() 156 + return len(state.busyProfiles) > 0 157 + } 158 + 159 + func setProfileBusy(idx int, active bool) { 160 + state.mu.Lock() 161 + applyBusy(idx, active) 162 + } 163 + 164 + func acquireProfile(idx int) bool { 165 + state.mu.Lock() 166 + if _, busy := state.busyProfiles[idx]; busy { 167 + state.mu.Unlock() 168 + return false 169 + } 170 + applyBusy(idx, true) 171 + return true 172 + } 173 + 174 + func applyBusy(idx int, active bool) { 175 + wasBusy := len(state.busyProfiles) > 0 176 + if active { 177 + state.busyProfiles[idx] = nil 178 + } else { 179 + delete(state.busyProfiles, idx) 180 + } 181 + nowBusy := len(state.busyProfiles) > 0 182 + if nowBusy && !wasBusy { 183 + setIconAnimated("busy") 184 + } 185 + cb := state.onBusyChanged 186 + state.mu.Unlock() 187 + if cb != nil { 188 + cb(nowBusy) 189 + } 190 + } 191 + 192 + func setProfileBusyCmd(idx int, cmd *exec.Cmd) { 193 + state.mu.Lock() 194 + defer state.mu.Unlock() 195 + state.busyProfiles[idx] = cmd 196 + } 197 + 198 + func cancelProfile(idx int) { 199 + state.mu.Lock() 200 + defer state.mu.Unlock() 201 + if cmd := state.busyProfiles[idx]; cmd != nil && cmd.Process != nil { 202 + interruptProcess(cmd.Process) 203 + } 204 + } 205 + 206 + func exitCode(err error) int { 207 + if exitErr, ok := err.(*exec.ExitError); ok { 208 + return exitErr.ExitCode() 209 + } 210 + return -1 211 + } 212 + 213 + func killedBySystem(err error) bool { 214 + return err != nil && exitCode(err) == -1 && strings.Contains(err.Error(), "killed") 215 + } 216 + 217 + func classifyError(err error, lastStderr string) (string, int) { 218 + code := exitCode(err) 219 + if killedBySystem(err) { 220 + return "Process terminated by system", code 221 + } 222 + if code == 12 { 223 + return "Wrong password", code 224 + } 225 + if lastStderr != "" { 226 + var msg struct { 227 + Message string `json:"message"` 228 + } 229 + if json.Unmarshal([]byte(lastStderr), &msg) == nil && msg.Message != "" { 230 + return msg.Message, code 231 + } 232 + return lastStderr, code 233 + } 234 + if err != nil { 235 + return err.Error(), code 236 + } 237 + return "", code 238 + }
-446
config.go
··· 1 - package main 2 - 3 - import ( 4 - "bufio" 5 - _ "embed" 6 - "fmt" 7 - "log" 8 - "os" 9 - "os/exec" 10 - "path/filepath" 11 - "runtime" 12 - "strings" 13 - "time" 14 - 15 - "github.com/BurntSushi/toml" 16 - "github.com/fsnotify/fsnotify" 17 - ) 18 - 19 - //go:embed templates/config.toml 20 - var defaultConfigTemplate string 21 - 22 - //go:embed templates/default.env 23 - var defaultEnvTemplate string 24 - 25 - type Schedule struct { 26 - Cron string `toml:"cron"` 27 - OnBattery bool `toml:"on_battery"` 28 - Backup *bool `toml:"backup"` 29 - Prune bool `toml:"prune"` 30 - Check bool `toml:"check"` 31 - } 32 - 33 - func (s Schedule) BackupEnabled() bool { 34 - return s.Backup == nil || *s.Backup 35 - } 36 - 37 - type Backup struct { 38 - Paths []string `toml:"paths"` 39 - Args []string `toml:"args"` 40 - ArgsScheduled []string `toml:"args_scheduled"` 41 - } 42 - 43 - type Prune struct { 44 - Args []string `toml:"args"` 45 - } 46 - 47 - type Check struct { 48 - Args []string `toml:"args"` 49 - } 50 - 51 - type Mount struct { 52 - Args []string `toml:"args"` 53 - } 54 - 55 - type Profile struct { 56 - Name string `toml:"name"` 57 - EnvFile string `toml:"env_file"` 58 - RetryLock string `toml:"retry_lock"` 59 - PreHook string `toml:"pre_hook"` 60 - PostHook string `toml:"post_hook"` 61 - Schedule Schedule `toml:"schedule"` 62 - Backup Backup `toml:"backup"` 63 - Prune Prune `toml:"prune"` 64 - Check Check `toml:"check"` 65 - Mount Mount `toml:"mount"` 66 - } 67 - 68 - type General struct { 69 - ManageRestic bool `toml:"manage_restic"` 70 - Notifications string `toml:"notifications"` 71 - ScheduleDisplay string `toml:"schedule_display"` 72 - Terminal string `toml:"terminal"` 73 - } 74 - 75 - type Config struct { 76 - General General `toml:"general"` 77 - Profile []Profile `toml:"profile"` 78 - } 79 - 80 - func parseEnvFile(path string) (map[string]string, error) { 81 - f, err := os.Open(path) 82 - if err != nil { 83 - return nil, err 84 - } 85 - defer f.Close() 86 - 87 - env := make(map[string]string) 88 - scanner := bufio.NewScanner(f) 89 - for scanner.Scan() { 90 - line := strings.TrimSpace(scanner.Text()) 91 - if line == "" || strings.HasPrefix(line, "#") { 92 - continue 93 - } 94 - line = strings.TrimPrefix(line, "export ") 95 - key, val, ok := strings.Cut(line, "=") 96 - if !ok { 97 - continue 98 - } 99 - key = strings.TrimSpace(key) 100 - val = strings.TrimSpace(val) 101 - if len(val) >= 2 && (val[0] == '"' || val[0] == '\'') && val[len(val)-1] == val[0] { 102 - val = val[1 : len(val)-1] 103 - } 104 - env[key] = val 105 - } 106 - return env, nil 107 - } 108 - 109 - func (prof Profile) configError() string { 110 - vars, err := parseEnvFile(prof.EnvFile) 111 - if err != nil { 112 - return "Env file not found" 113 - } 114 - if vars["RESTIC_REPOSITORY"] == "" { 115 - return "RESTIC_REPOSITORY not set in env file" 116 - } 117 - if vars["RESTIC_PASSWORD"] == "" && vars["RESTIC_PASSWORD_FILE"] == "" && vars["RESTIC_PASSWORD_COMMAND"] == "" { 118 - return "No password set in env file" 119 - } 120 - if len(prof.Backup.Paths) == 0 { 121 - return "No paths configured" 122 - } 123 - return "" 124 - } 125 - 126 - func (prof Profile) displayName() string { 127 - if prof.Name != "" { 128 - return prof.Name 129 - } 130 - if prof.EnvFile != "" { 131 - return strings.TrimSuffix(filepath.Base(prof.EnvFile), ".env") 132 - } 133 - return "Profile" 134 - } 135 - 136 - func needsFullDiskAccess() bool { 137 - if runtime.GOOS != "darwin" { 138 - return false 139 - } 140 - f, err := os.Open("/Library/Application Support/com.apple.TCC/TCC.db") // TODO: is this a good idea or should i be doing something else for FDA detection?? appears to work fine 141 - if err == nil { 142 - f.Close() 143 - } 144 - return os.IsPermission(err) 145 - } 146 - 147 - func openFullDiskAccessSettings() { 148 - exec.Command("open", "x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles").Start() 149 - } 150 - 151 - func promptFullDiskAccess() { 152 - out, err := exec.Command("osascript", "-e", 153 - `display dialog "Restray needs Full Disk Access to be able to back up all your files." buttons {"Skip", "Open Settings"} default button "Open Settings" with title "Restray" with icon caution`).Output() 154 - if err == nil && strings.Contains(string(out), "Open Settings") { 155 - openFullDiskAccessSettings() 156 - } 157 - } 158 - 159 - func envFilesInsecure(profiles []Profile) bool { 160 - if runtime.GOOS == "windows" { 161 - return false 162 - } 163 - for _, p := range profiles { 164 - info, err := os.Stat(p.EnvFile) 165 - if err == nil && info.Mode().Perm()&0077 != 0 { 166 - return true 167 - } 168 - } 169 - return false 170 - } 171 - 172 - func fixEnvFilePermissions(profiles []Profile) { 173 - for _, p := range profiles { 174 - os.Chmod(p.EnvFile, 0600) 175 - } 176 - } 177 - 178 - func configDir() string { 179 - dir, _ := os.UserConfigDir() 180 - return filepath.Join(dir, "restray") 181 - } 182 - 183 - func configPath() string { return filepath.Join(configDir(), "config.toml") } 184 - func defaultEnvPath(name string) string { 185 - if name == "" { 186 - name = "default" 187 - } 188 - return filepath.Join(configDir(), name+".env") 189 - } 190 - 191 - func logPath() string { return filepath.Join(configDir(), "restray.log") } 192 - func lockPath() string { return filepath.Join(configDir(), "restray.lock") } 193 - 194 - func loadConfig() Config { 195 - cfg := Config{} 196 - if _, err := toml.DecodeFile(configPath(), &cfg); err != nil && !os.IsNotExist(err) { 197 - log.Printf("config: %v", err) 198 - } 199 - if cfg.General.ScheduleDisplay == "" { 200 - cfg.General.ScheduleDisplay = "description" 201 - } 202 - if len(cfg.Profile) == 0 { 203 - cfg.Profile = append(cfg.Profile, Profile{Name: "default"}) 204 - } 205 - for i := range cfg.Profile { 206 - prof := &cfg.Profile[i] 207 - if prof.EnvFile == "" { 208 - prof.EnvFile = defaultEnvPath(prof.Name) 209 - } 210 - if prof.RetryLock == "" { 211 - prof.RetryLock = "2m" 212 - } 213 - if prof.Backup.Args == nil { 214 - prof.Backup.Args = []string{"--exclude-caches", "--exclude", "*.tmp"} 215 - } 216 - if prof.Backup.ArgsScheduled == nil { 217 - prof.Backup.ArgsScheduled = []string{"--tag", "scheduled"} 218 - } 219 - if prof.Prune.Args == nil { 220 - prof.Prune.Args = []string{ 221 - "--keep-last", "4", 222 - "--keep-hourly", "24", 223 - "--keep-daily", "7", 224 - "--keep-weekly", "4", 225 - "--keep-monthly", "12", 226 - } 227 - } 228 - } 229 - return cfg 230 - } 231 - 232 - func defaultConfigContents() string { 233 - return strings.ReplaceAll(defaultConfigTemplate, "{{ENV_FILE}}", filepath.ToSlash(defaultEnvPath("default"))) 234 - } 235 - 236 - func ensureConfigFile() { 237 - p := configPath() 238 - if _, err := os.Stat(p); err == nil { 239 - return 240 - } 241 - os.MkdirAll(filepath.Dir(p), 0700) 242 - os.WriteFile(p, []byte(defaultConfigContents()), 0600) 243 - } 244 - 245 - func ensureEnvFile(path string) { 246 - if _, err := os.Stat(path); err == nil { 247 - return 248 - } 249 - os.MkdirAll(filepath.Dir(path), 0700) 250 - os.WriteFile(path, []byte(defaultEnvTemplate), 0600) 251 - } 252 - 253 - func openFile(path string) { 254 - var cmd *exec.Cmd 255 - switch runtime.GOOS { 256 - case "darwin": 257 - cmd = exec.Command("open", path) 258 - case "windows": 259 - cmd = exec.Command("rundll32", "url.dll,FileProtocolHandler", path) 260 - default: 261 - cmd = exec.Command("xdg-open", path) 262 - } 263 - cmd.Start() 264 - } 265 - 266 - func openInEditor(path string) { 267 - if runtime.GOOS == "darwin" { 268 - exec.Command("open", "-t", path).Start() 269 - return 270 - } 271 - openFile(path) 272 - } 273 - 274 - func openConsole(prof Profile, profileCount int, terminal, tailFile string) { 275 - vars, err := parseEnvFile(prof.EnvFile) 276 - if err != nil { 277 - log.Printf("console: failed to read env file: %v", err) 278 - return 279 - } 280 - env := os.Environ() 281 - for k, v := range vars { 282 - env = append(env, k+"="+v) 283 - } 284 - if p, _ := findRestic(); p != "" { 285 - dir := filepath.Dir(p) 286 - for i, e := range env { 287 - if strings.HasPrefix(e, "PATH=") { 288 - env[i] = "PATH=" + dir + string(os.PathListSeparator) + e[5:] 289 - break 290 - } 291 - } 292 - } 293 - 294 - consoleMsg := "Restray: environment loaded in shell. Run \"restic\" for commands." 295 - if profileCount > 1 { 296 - consoleMsg = fmt.Sprintf("Restray: environment for profile \"%s\" loaded in shell. Run \"restic\" for commands.", prof.displayName()) 297 - } 298 - tailMsg := "Restray: tailing log." 299 - 300 - var cmd *exec.Cmd 301 - switch runtime.GOOS { 302 - case "darwin": 303 - script, err := os.CreateTemp("", "restray-shell-*.sh") 304 - if err != nil { 305 - return 306 - } 307 - shell := os.Getenv("SHELL") 308 - if shell == "" { 309 - shell = "/bin/zsh" 310 - } 311 - fmt.Fprintf(script, "#!/bin/sh\n") 312 - for k, v := range vars { 313 - fmt.Fprintf(script, "export %s=%q\n", k, v) 314 - } 315 - if p, _ := findRestic(); p != "" { 316 - fmt.Fprintf(script, "export PATH=%q:\"$PATH\"\n", filepath.Dir(p)) 317 - } 318 - fmt.Fprintf(script, "clear\n") 319 - fmt.Fprintf(script, "rm -f %q\n", script.Name()) 320 - fmt.Fprintf(script, "echo '%s'\n", consoleMsg) 321 - if tailFile != "" { 322 - fmt.Fprintf(script, "echo '%s'\n", tailMsg) 323 - fmt.Fprintf(script, "tail -n +1 -f %q &\n", tailFile) 324 - fmt.Fprintf(script, "TAIL_PID=$!\n") 325 - fmt.Fprintf(script, "trap 'kill $TAIL_PID 2>/dev/null; wait $TAIL_PID 2>/dev/null' INT\n") 326 - fmt.Fprintf(script, "wait $TAIL_PID\n") 327 - } 328 - fmt.Fprintf(script, "cd %q\n", configDir()) 329 - fmt.Fprintf(script, "exec %s -l\n", shell) 330 - script.Close() 331 - os.Chmod(script.Name(), 0700) 332 - cmd = exec.Command("open", "-a", darwinTerminalApp(terminal), script.Name()) 333 - case "windows": 334 - script, err := os.CreateTemp("", "restray-shell-*.bat") 335 - if err != nil { 336 - return 337 - } 338 - fmt.Fprintf(script, "@echo off\r\n") 339 - for k, v := range vars { 340 - fmt.Fprintf(script, "set %s=%s\r\n", k, v) 341 - } 342 - if p, _ := findRestic(); p != "" { 343 - fmt.Fprintf(script, "set PATH=%s;%%PATH%%\r\n", filepath.Dir(p)) 344 - } 345 - fmt.Fprintf(script, "echo %s\r\n", consoleMsg) 346 - if tailFile != "" { 347 - fmt.Fprintf(script, "echo %s\r\n", tailMsg) 348 - fmt.Fprintf(script, "echo.\r\n") 349 - fmt.Fprintf(script, "powershell -Command \"Get-Content -Path '%s' -Wait\"\r\n", tailFile) 350 - } 351 - fmt.Fprintf(script, "cd /d %q\r\n", configDir()) 352 - fmt.Fprintf(script, "cmd /k\r\n") 353 - script.Close() 354 - cmd = exec.Command("cmd", "/c", "start", "", script.Name()) 355 - showConsole(cmd) 356 - default: 357 - shell := os.Getenv("SHELL") 358 - if shell == "" { 359 - shell = "/bin/sh" 360 - } 361 - term := terminal 362 - if term == "" { 363 - term = findTerminal() 364 - } 365 - if term == "" { 366 - log.Print("console: no terminal emulator found") 367 - return 368 - } 369 - var script string 370 - if tailFile != "" { 371 - script = fmt.Sprintf("cd %q; clear; echo '%s'; echo '%s'; tail -n +1 -f %q; exec %s", configDir(), consoleMsg, tailMsg, tailFile, shell) 372 - } else { 373 - script = fmt.Sprintf("cd %q; clear; echo '%s'; exec %s", configDir(), consoleMsg, shell) 374 - } 375 - cmd = exec.Command(term, "-e", shell, "-c", script) 376 - cmd.Env = env 377 - } 378 - cmd.Start() 379 - } 380 - 381 - func fileExists(path string) bool { 382 - _, err := os.Stat(path) 383 - return err == nil 384 - } 385 - 386 - func darwinTerminalApp(terminal string) string { 387 - t := strings.TrimSuffix(strings.ToLower(terminal), ".app") 388 - switch t { 389 - case "", "terminal": 390 - return "Terminal" 391 - case "iterm", "iterm2": 392 - return "iTerm" 393 - case "alacritty": 394 - return "Alacritty" 395 - case "kitty": 396 - return "kitty" 397 - case "ghostty": 398 - return "Ghostty" 399 - case "wezterm": 400 - return "WezTerm" 401 - default: 402 - return terminal 403 - } 404 - } 405 - 406 - func findTerminal() string { 407 - if t := os.Getenv("TERMINAL"); t != "" { 408 - return t 409 - } 410 - for _, t := range []string{"x-terminal-emulator", "gnome-terminal", "konsole", "xfce4-terminal", "xterm"} { 411 - if p, err := exec.LookPath(t); err == nil { 412 - return p 413 - } 414 - } 415 - return "" 416 - } 417 - 418 - func watchConfig(onChange func()) { 419 - watcher, err := fsnotify.NewWatcher() 420 - if err != nil { 421 - return 422 - } 423 - watcher.Add(configDir()) 424 - 425 - go func() { 426 - for range watcher.Errors { 427 - } 428 - }() 429 - go func() { 430 - var debounce *time.Timer 431 - for event := range watcher.Events { 432 - ext := filepath.Ext(event.Name) 433 - if ext != ".toml" && ext != ".env" { 434 - continue 435 - } 436 - action := event.Has(fsnotify.Write) || event.Has(fsnotify.Create) || 437 - event.Has(fsnotify.Remove) || event.Has(fsnotify.Rename) 438 - if action { 439 - if debounce != nil { 440 - debounce.Stop() 441 - } 442 - debounce = time.AfterFunc(200*time.Millisecond, onChange) 443 - } 444 - } 445 - }() 446 - }
+1 -2
exec_other.go cmd/restray/exec_other.go
··· 8 8 "syscall" 9 9 ) 10 10 11 - func hideWindow(_ *exec.Cmd) {} 12 - func showConsole(_ *exec.Cmd) {} 11 + func hideWindow(_ *exec.Cmd) {} 13 12 14 13 func interruptProcess(p *os.Process) { 15 14 p.Signal(os.Interrupt)
exec_windows.go cmd/restray/exec_windows.go
+1
flake.nix
··· 20 20 version = "0.6.1"; 21 21 src = ./.; 22 22 vendorHash = "sha256-iYM2yvcTsYqn1pyB7jrqroYUfZM4ysWZnXKiLRVqkT0="; 23 + subPackages = ["cmd/restray"]; 23 24 24 25 ldflags = ["-X" "main.resticBuiltinPath=${pkgs.restic}/bin/restic"]; 25 26
+1 -1
icons.go cmd/restray/icons.go
··· 89 89 90 90 var alertIcon []byte 91 91 92 - func init() { 92 + func initAlertIcon() { 93 93 frames := loadIcons(iconVariant() + "-fail-") 94 94 if len(frames) > 0 { 95 95 alertIcon = frames[len(frames)-1]
icons/busy.png cmd/restray/icons/busy.png
icons/download.png cmd/restray/icons/download.png
icons/fail.png cmd/restray/icons/fail.png
icons/generated/linux-busy-00.png cmd/restray/icons/generated/linux-busy-00.png
icons/generated/linux-busy-01.png cmd/restray/icons/generated/linux-busy-01.png
icons/generated/linux-busy-02.png cmd/restray/icons/generated/linux-busy-02.png
icons/generated/linux-busy-03.png cmd/restray/icons/generated/linux-busy-03.png
icons/generated/linux-busy-04.png cmd/restray/icons/generated/linux-busy-04.png
icons/generated/linux-busy-05.png cmd/restray/icons/generated/linux-busy-05.png
icons/generated/linux-busy-06.png cmd/restray/icons/generated/linux-busy-06.png
icons/generated/linux-busy-07.png cmd/restray/icons/generated/linux-busy-07.png
icons/generated/linux-busy-08.png cmd/restray/icons/generated/linux-busy-08.png
icons/generated/linux-busy-09.png cmd/restray/icons/generated/linux-busy-09.png
icons/generated/linux-busy-10.png cmd/restray/icons/generated/linux-busy-10.png
icons/generated/linux-busy-11.png cmd/restray/icons/generated/linux-busy-11.png
icons/generated/linux-busy-12.png cmd/restray/icons/generated/linux-busy-12.png
icons/generated/linux-busy-13.png cmd/restray/icons/generated/linux-busy-13.png
icons/generated/linux-busy-14.png cmd/restray/icons/generated/linux-busy-14.png
icons/generated/linux-busy-15.png cmd/restray/icons/generated/linux-busy-15.png
icons/generated/linux-busy-16.png cmd/restray/icons/generated/linux-busy-16.png
icons/generated/linux-download-00.png cmd/restray/icons/generated/linux-download-00.png
icons/generated/linux-download-01.png cmd/restray/icons/generated/linux-download-01.png
icons/generated/linux-download-02.png cmd/restray/icons/generated/linux-download-02.png
icons/generated/linux-download-03.png cmd/restray/icons/generated/linux-download-03.png
icons/generated/linux-download-04.png cmd/restray/icons/generated/linux-download-04.png
icons/generated/linux-download-05.png cmd/restray/icons/generated/linux-download-05.png
icons/generated/linux-download-06.png cmd/restray/icons/generated/linux-download-06.png
icons/generated/linux-download-07.png cmd/restray/icons/generated/linux-download-07.png
icons/generated/linux-download-08.png cmd/restray/icons/generated/linux-download-08.png
icons/generated/linux-download-09.png cmd/restray/icons/generated/linux-download-09.png
icons/generated/linux-download-10.png cmd/restray/icons/generated/linux-download-10.png
icons/generated/linux-download-11.png cmd/restray/icons/generated/linux-download-11.png
icons/generated/linux-download-12.png cmd/restray/icons/generated/linux-download-12.png
icons/generated/linux-download-13.png cmd/restray/icons/generated/linux-download-13.png
icons/generated/linux-download-14.png cmd/restray/icons/generated/linux-download-14.png
icons/generated/linux-download-15.png cmd/restray/icons/generated/linux-download-15.png
icons/generated/linux-download-16.png cmd/restray/icons/generated/linux-download-16.png
icons/generated/linux-download-17.png cmd/restray/icons/generated/linux-download-17.png
icons/generated/linux-download-18.png cmd/restray/icons/generated/linux-download-18.png
icons/generated/linux-download-19.png cmd/restray/icons/generated/linux-download-19.png
icons/generated/linux-download-20.png cmd/restray/icons/generated/linux-download-20.png
icons/generated/linux-download-21.png cmd/restray/icons/generated/linux-download-21.png
icons/generated/linux-download-22.png cmd/restray/icons/generated/linux-download-22.png
icons/generated/linux-download-23.png cmd/restray/icons/generated/linux-download-23.png
icons/generated/linux-download-24.png cmd/restray/icons/generated/linux-download-24.png
icons/generated/linux-download-25.png cmd/restray/icons/generated/linux-download-25.png
icons/generated/linux-download-26.png cmd/restray/icons/generated/linux-download-26.png
icons/generated/linux-download-27.png cmd/restray/icons/generated/linux-download-27.png
icons/generated/linux-download-28.png cmd/restray/icons/generated/linux-download-28.png
icons/generated/linux-download-29.png cmd/restray/icons/generated/linux-download-29.png
icons/generated/linux-download-30.png cmd/restray/icons/generated/linux-download-30.png
icons/generated/linux-download-31.png cmd/restray/icons/generated/linux-download-31.png
icons/generated/linux-fail-00.png cmd/restray/icons/generated/linux-fail-00.png
icons/generated/linux-fail-01.png cmd/restray/icons/generated/linux-fail-01.png
icons/generated/linux-fail-02.png cmd/restray/icons/generated/linux-fail-02.png
icons/generated/linux-fail-03.png cmd/restray/icons/generated/linux-fail-03.png
icons/generated/linux-fail-04.png cmd/restray/icons/generated/linux-fail-04.png
icons/generated/linux-fail-05.png cmd/restray/icons/generated/linux-fail-05.png
icons/generated/linux-fail-06.png cmd/restray/icons/generated/linux-fail-06.png
icons/generated/linux-fail-07.png cmd/restray/icons/generated/linux-fail-07.png
icons/generated/linux-fail-08.png cmd/restray/icons/generated/linux-fail-08.png
icons/generated/linux-fail-09.png cmd/restray/icons/generated/linux-fail-09.png
icons/generated/linux-fail-10.png cmd/restray/icons/generated/linux-fail-10.png
icons/generated/linux-fail-11.png cmd/restray/icons/generated/linux-fail-11.png
icons/generated/linux-fail-12.png cmd/restray/icons/generated/linux-fail-12.png
icons/generated/linux-fail-13.png cmd/restray/icons/generated/linux-fail-13.png
icons/generated/linux-fail-14.png cmd/restray/icons/generated/linux-fail-14.png
icons/generated/linux-idle-00.png cmd/restray/icons/generated/linux-idle-00.png
icons/generated/linux-idle-01.png cmd/restray/icons/generated/linux-idle-01.png
icons/generated/linux-idle-02.png cmd/restray/icons/generated/linux-idle-02.png
icons/generated/linux-idle-03.png cmd/restray/icons/generated/linux-idle-03.png
icons/generated/linux-idle-04.png cmd/restray/icons/generated/linux-idle-04.png
icons/generated/linux-idle-05.png cmd/restray/icons/generated/linux-idle-05.png
icons/generated/linux-idle-06.png cmd/restray/icons/generated/linux-idle-06.png
icons/generated/linux-idle-07.png cmd/restray/icons/generated/linux-idle-07.png
icons/generated/linux-idle-08.png cmd/restray/icons/generated/linux-idle-08.png
icons/generated/linux-idle-09.png cmd/restray/icons/generated/linux-idle-09.png
icons/generated/linux-idle-10.png cmd/restray/icons/generated/linux-idle-10.png
icons/generated/linux-idle-11.png cmd/restray/icons/generated/linux-idle-11.png
icons/generated/linux-idle-12.png cmd/restray/icons/generated/linux-idle-12.png
icons/generated/macos-busy-00.png cmd/restray/icons/generated/macos-busy-00.png
icons/generated/macos-busy-01.png cmd/restray/icons/generated/macos-busy-01.png
icons/generated/macos-busy-02.png cmd/restray/icons/generated/macos-busy-02.png
icons/generated/macos-busy-03.png cmd/restray/icons/generated/macos-busy-03.png
icons/generated/macos-busy-04.png cmd/restray/icons/generated/macos-busy-04.png
icons/generated/macos-busy-05.png cmd/restray/icons/generated/macos-busy-05.png
icons/generated/macos-busy-06.png cmd/restray/icons/generated/macos-busy-06.png
icons/generated/macos-busy-07.png cmd/restray/icons/generated/macos-busy-07.png
icons/generated/macos-busy-08.png cmd/restray/icons/generated/macos-busy-08.png
icons/generated/macos-busy-09.png cmd/restray/icons/generated/macos-busy-09.png
icons/generated/macos-busy-10.png cmd/restray/icons/generated/macos-busy-10.png
icons/generated/macos-busy-11.png cmd/restray/icons/generated/macos-busy-11.png
icons/generated/macos-busy-12.png cmd/restray/icons/generated/macos-busy-12.png
icons/generated/macos-busy-13.png cmd/restray/icons/generated/macos-busy-13.png
icons/generated/macos-busy-14.png cmd/restray/icons/generated/macos-busy-14.png
icons/generated/macos-busy-15.png cmd/restray/icons/generated/macos-busy-15.png
icons/generated/macos-busy-16.png cmd/restray/icons/generated/macos-busy-16.png
icons/generated/macos-download-00.png cmd/restray/icons/generated/macos-download-00.png
icons/generated/macos-download-01.png cmd/restray/icons/generated/macos-download-01.png
icons/generated/macos-download-02.png cmd/restray/icons/generated/macos-download-02.png
icons/generated/macos-download-03.png cmd/restray/icons/generated/macos-download-03.png
icons/generated/macos-download-04.png cmd/restray/icons/generated/macos-download-04.png
icons/generated/macos-download-05.png cmd/restray/icons/generated/macos-download-05.png
icons/generated/macos-download-06.png cmd/restray/icons/generated/macos-download-06.png
icons/generated/macos-download-07.png cmd/restray/icons/generated/macos-download-07.png
icons/generated/macos-download-08.png cmd/restray/icons/generated/macos-download-08.png
icons/generated/macos-download-09.png cmd/restray/icons/generated/macos-download-09.png
icons/generated/macos-download-10.png cmd/restray/icons/generated/macos-download-10.png
icons/generated/macos-download-11.png cmd/restray/icons/generated/macos-download-11.png
icons/generated/macos-download-12.png cmd/restray/icons/generated/macos-download-12.png
icons/generated/macos-download-13.png cmd/restray/icons/generated/macos-download-13.png
icons/generated/macos-download-14.png cmd/restray/icons/generated/macos-download-14.png
icons/generated/macos-download-15.png cmd/restray/icons/generated/macos-download-15.png
icons/generated/macos-download-16.png cmd/restray/icons/generated/macos-download-16.png
icons/generated/macos-download-17.png cmd/restray/icons/generated/macos-download-17.png
icons/generated/macos-download-18.png cmd/restray/icons/generated/macos-download-18.png
icons/generated/macos-download-19.png cmd/restray/icons/generated/macos-download-19.png
icons/generated/macos-download-20.png cmd/restray/icons/generated/macos-download-20.png
icons/generated/macos-download-21.png cmd/restray/icons/generated/macos-download-21.png
icons/generated/macos-download-22.png cmd/restray/icons/generated/macos-download-22.png
icons/generated/macos-download-23.png cmd/restray/icons/generated/macos-download-23.png
icons/generated/macos-download-24.png cmd/restray/icons/generated/macos-download-24.png
icons/generated/macos-download-25.png cmd/restray/icons/generated/macos-download-25.png
icons/generated/macos-download-26.png cmd/restray/icons/generated/macos-download-26.png
icons/generated/macos-download-27.png cmd/restray/icons/generated/macos-download-27.png
icons/generated/macos-download-28.png cmd/restray/icons/generated/macos-download-28.png
icons/generated/macos-download-29.png cmd/restray/icons/generated/macos-download-29.png
icons/generated/macos-download-30.png cmd/restray/icons/generated/macos-download-30.png
icons/generated/macos-download-31.png cmd/restray/icons/generated/macos-download-31.png
icons/generated/macos-fail-00.png cmd/restray/icons/generated/macos-fail-00.png
icons/generated/macos-fail-01.png cmd/restray/icons/generated/macos-fail-01.png
icons/generated/macos-fail-02.png cmd/restray/icons/generated/macos-fail-02.png
icons/generated/macos-fail-03.png cmd/restray/icons/generated/macos-fail-03.png
icons/generated/macos-fail-04.png cmd/restray/icons/generated/macos-fail-04.png
icons/generated/macos-fail-05.png cmd/restray/icons/generated/macos-fail-05.png
icons/generated/macos-fail-06.png cmd/restray/icons/generated/macos-fail-06.png
icons/generated/macos-fail-07.png cmd/restray/icons/generated/macos-fail-07.png
icons/generated/macos-fail-08.png cmd/restray/icons/generated/macos-fail-08.png
icons/generated/macos-fail-09.png cmd/restray/icons/generated/macos-fail-09.png
icons/generated/macos-fail-10.png cmd/restray/icons/generated/macos-fail-10.png
icons/generated/macos-fail-11.png cmd/restray/icons/generated/macos-fail-11.png
icons/generated/macos-fail-12.png cmd/restray/icons/generated/macos-fail-12.png
icons/generated/macos-fail-13.png cmd/restray/icons/generated/macos-fail-13.png
icons/generated/macos-fail-14.png cmd/restray/icons/generated/macos-fail-14.png
icons/generated/macos-idle-00.png cmd/restray/icons/generated/macos-idle-00.png
icons/generated/macos-idle-01.png cmd/restray/icons/generated/macos-idle-01.png
icons/generated/macos-idle-02.png cmd/restray/icons/generated/macos-idle-02.png
icons/generated/macos-idle-03.png cmd/restray/icons/generated/macos-idle-03.png
icons/generated/macos-idle-04.png cmd/restray/icons/generated/macos-idle-04.png
icons/generated/macos-idle-05.png cmd/restray/icons/generated/macos-idle-05.png
icons/generated/macos-idle-06.png cmd/restray/icons/generated/macos-idle-06.png
icons/generated/macos-idle-07.png cmd/restray/icons/generated/macos-idle-07.png
icons/generated/macos-idle-08.png cmd/restray/icons/generated/macos-idle-08.png
icons/generated/macos-idle-09.png cmd/restray/icons/generated/macos-idle-09.png
icons/generated/macos-idle-10.png cmd/restray/icons/generated/macos-idle-10.png
icons/generated/macos-idle-11.png cmd/restray/icons/generated/macos-idle-11.png
icons/generated/macos-idle-12.png cmd/restray/icons/generated/macos-idle-12.png
icons/generated/windows-busy-00.ico cmd/restray/icons/generated/windows-busy-00.ico
icons/generated/windows-busy-01.ico cmd/restray/icons/generated/windows-busy-01.ico
icons/generated/windows-busy-02.ico cmd/restray/icons/generated/windows-busy-02.ico
icons/generated/windows-busy-03.ico cmd/restray/icons/generated/windows-busy-03.ico
icons/generated/windows-busy-04.ico cmd/restray/icons/generated/windows-busy-04.ico
icons/generated/windows-busy-05.ico cmd/restray/icons/generated/windows-busy-05.ico
icons/generated/windows-busy-06.ico cmd/restray/icons/generated/windows-busy-06.ico
icons/generated/windows-busy-07.ico cmd/restray/icons/generated/windows-busy-07.ico
icons/generated/windows-busy-08.ico cmd/restray/icons/generated/windows-busy-08.ico
icons/generated/windows-busy-09.ico cmd/restray/icons/generated/windows-busy-09.ico
icons/generated/windows-busy-10.ico cmd/restray/icons/generated/windows-busy-10.ico
icons/generated/windows-busy-11.ico cmd/restray/icons/generated/windows-busy-11.ico
icons/generated/windows-busy-12.ico cmd/restray/icons/generated/windows-busy-12.ico
icons/generated/windows-busy-13.ico cmd/restray/icons/generated/windows-busy-13.ico
icons/generated/windows-busy-14.ico cmd/restray/icons/generated/windows-busy-14.ico
icons/generated/windows-busy-15.ico cmd/restray/icons/generated/windows-busy-15.ico
icons/generated/windows-busy-16.ico cmd/restray/icons/generated/windows-busy-16.ico
icons/generated/windows-download-00.ico cmd/restray/icons/generated/windows-download-00.ico
icons/generated/windows-download-01.ico cmd/restray/icons/generated/windows-download-01.ico
icons/generated/windows-download-02.ico cmd/restray/icons/generated/windows-download-02.ico
icons/generated/windows-download-03.ico cmd/restray/icons/generated/windows-download-03.ico
icons/generated/windows-download-04.ico cmd/restray/icons/generated/windows-download-04.ico
icons/generated/windows-download-05.ico cmd/restray/icons/generated/windows-download-05.ico
icons/generated/windows-download-06.ico cmd/restray/icons/generated/windows-download-06.ico
icons/generated/windows-download-07.ico cmd/restray/icons/generated/windows-download-07.ico
icons/generated/windows-download-08.ico cmd/restray/icons/generated/windows-download-08.ico
icons/generated/windows-download-09.ico cmd/restray/icons/generated/windows-download-09.ico
icons/generated/windows-download-10.ico cmd/restray/icons/generated/windows-download-10.ico
icons/generated/windows-download-11.ico cmd/restray/icons/generated/windows-download-11.ico
icons/generated/windows-download-12.ico cmd/restray/icons/generated/windows-download-12.ico
icons/generated/windows-download-13.ico cmd/restray/icons/generated/windows-download-13.ico
icons/generated/windows-download-14.ico cmd/restray/icons/generated/windows-download-14.ico
icons/generated/windows-download-15.ico cmd/restray/icons/generated/windows-download-15.ico
icons/generated/windows-download-16.ico cmd/restray/icons/generated/windows-download-16.ico
icons/generated/windows-download-17.ico cmd/restray/icons/generated/windows-download-17.ico
icons/generated/windows-download-18.ico cmd/restray/icons/generated/windows-download-18.ico
icons/generated/windows-download-19.ico cmd/restray/icons/generated/windows-download-19.ico
icons/generated/windows-download-20.ico cmd/restray/icons/generated/windows-download-20.ico
icons/generated/windows-download-21.ico cmd/restray/icons/generated/windows-download-21.ico
icons/generated/windows-download-22.ico cmd/restray/icons/generated/windows-download-22.ico
icons/generated/windows-download-23.ico cmd/restray/icons/generated/windows-download-23.ico
icons/generated/windows-download-24.ico cmd/restray/icons/generated/windows-download-24.ico
icons/generated/windows-download-25.ico cmd/restray/icons/generated/windows-download-25.ico
icons/generated/windows-download-26.ico cmd/restray/icons/generated/windows-download-26.ico
icons/generated/windows-download-27.ico cmd/restray/icons/generated/windows-download-27.ico
icons/generated/windows-download-28.ico cmd/restray/icons/generated/windows-download-28.ico
icons/generated/windows-download-29.ico cmd/restray/icons/generated/windows-download-29.ico
icons/generated/windows-download-30.ico cmd/restray/icons/generated/windows-download-30.ico
icons/generated/windows-download-31.ico cmd/restray/icons/generated/windows-download-31.ico
icons/generated/windows-fail-00.ico cmd/restray/icons/generated/windows-fail-00.ico
icons/generated/windows-fail-01.ico cmd/restray/icons/generated/windows-fail-01.ico
icons/generated/windows-fail-02.ico cmd/restray/icons/generated/windows-fail-02.ico
icons/generated/windows-fail-03.ico cmd/restray/icons/generated/windows-fail-03.ico
icons/generated/windows-fail-04.ico cmd/restray/icons/generated/windows-fail-04.ico
icons/generated/windows-fail-05.ico cmd/restray/icons/generated/windows-fail-05.ico
icons/generated/windows-fail-06.ico cmd/restray/icons/generated/windows-fail-06.ico
icons/generated/windows-fail-07.ico cmd/restray/icons/generated/windows-fail-07.ico
icons/generated/windows-fail-08.ico cmd/restray/icons/generated/windows-fail-08.ico
icons/generated/windows-fail-09.ico cmd/restray/icons/generated/windows-fail-09.ico
icons/generated/windows-fail-10.ico cmd/restray/icons/generated/windows-fail-10.ico
icons/generated/windows-fail-11.ico cmd/restray/icons/generated/windows-fail-11.ico
icons/generated/windows-fail-12.ico cmd/restray/icons/generated/windows-fail-12.ico
icons/generated/windows-fail-13.ico cmd/restray/icons/generated/windows-fail-13.ico
icons/generated/windows-fail-14.ico cmd/restray/icons/generated/windows-fail-14.ico
icons/generated/windows-idle-00.ico cmd/restray/icons/generated/windows-idle-00.ico
icons/generated/windows-idle-01.ico cmd/restray/icons/generated/windows-idle-01.ico
icons/generated/windows-idle-02.ico cmd/restray/icons/generated/windows-idle-02.ico
icons/generated/windows-idle-03.ico cmd/restray/icons/generated/windows-idle-03.ico
icons/generated/windows-idle-04.ico cmd/restray/icons/generated/windows-idle-04.ico
icons/generated/windows-idle-05.ico cmd/restray/icons/generated/windows-idle-05.ico
icons/generated/windows-idle-06.ico cmd/restray/icons/generated/windows-idle-06.ico
icons/generated/windows-idle-07.ico cmd/restray/icons/generated/windows-idle-07.ico
icons/generated/windows-idle-08.ico cmd/restray/icons/generated/windows-idle-08.ico
icons/generated/windows-idle-09.ico cmd/restray/icons/generated/windows-idle-09.ico
icons/generated/windows-idle-10.ico cmd/restray/icons/generated/windows-idle-10.ico
icons/generated/windows-idle-11.ico cmd/restray/icons/generated/windows-idle-11.ico
icons/generated/windows-idle-12.ico cmd/restray/icons/generated/windows-idle-12.ico
icons/idle.png cmd/restray/icons/idle.png
+22 -22
justfile
··· 1 1 app := "Restray" 2 - version := "0.6.1" 2 + version := `grep 'const version' cmd/restray/main.go | cut -d'"' -f2` 3 3 bin := "bin" 4 4 dist := "dist" 5 + pkg := "cmd/restray" 5 6 6 7 icons: 7 8 #!/usr/bin/env bash 8 9 set -e 9 - out="icons/generated" 10 + tmp=$(mktemp -d) 11 + trap 'rm -rf "$tmp"' EXIT 12 + out="{{pkg}}/icons/generated" 10 13 rm -rf "$out" 11 14 mkdir -p "$out" 12 15 for sheet in idle busy fail download; do 13 - src="icons/$sheet.png" 16 + src="{{pkg}}/icons/$sheet.png" 14 17 [ -f "$src" ] || continue 15 18 dims=$(magick identify -format "%w %h" "$src") 16 19 w=${dims% *} ··· 19 22 for ((i=0; i<cols; i++)); do 20 23 x=$((i * sz)) 21 24 n=$(printf "%02d" "$i") 22 - magick "$src" -crop "${sz}x${sz}+${x}+0" +repage /tmp/restray-frame.png 23 - cp /tmp/restray-frame.png "$out/linux-${sheet}-${n}.png" 24 - magick /tmp/restray-frame.png -alpha extract -background black -alpha shape "$out/macos-${sheet}-${n}.png" 25 - magick /tmp/restray-frame.png -define icon:auto-resize=64,48,32,16 "$out/windows-${sheet}-${n}.ico" 25 + magick "$src" -crop "${sz}x${sz}+${x}+0" +repage "$tmp/frame.png" 26 + cp "$tmp/frame.png" "$out/linux-${sheet}-${n}.png" 27 + magick "$tmp/frame.png" -alpha extract -background black -alpha shape "$out/macos-${sheet}-${n}.png" 28 + magick "$tmp/frame.png" -define icon:auto-resize=64,48,32,16 "$out/windows-${sheet}-${n}.ico" 26 29 done 27 30 done 28 - rm -f /tmp/restray-frame.png 29 31 # Last idle frame = app icon for packaging 30 32 last=$(find "$out" -name "linux-idle-*.png" 2>/dev/null | sort | tail -1) 31 33 if [ -n "$last" ]; then ··· 35 37 rm -f packaging/windows/restray_windows_386.syso 36 38 sizes="" 37 39 for size in 16 32 128 256 512; do 38 - magick "$last" -resize ${size}x${size} "/tmp/restray-${size}.png" 39 - sizes="$sizes /tmp/restray-${size}.png" 40 + magick "$last" -resize ${size}x${size} "$tmp/restray-${size}.png" 41 + sizes="$sizes $tmp/restray-${size}.png" 40 42 done 41 43 png2icns packaging/darwin/restray.icns $sizes 42 - rm -f $sizes 43 44 fi 44 45 45 46 build target="" arch="": ··· 58 59 darwin) 59 60 export CGO_CFLAGS="-mmacosx-version-min=11.0" 60 61 export CGO_LDFLAGS="-mmacosx-version-min=11.0" 61 - GOOS=darwin GOARCH=$arch go build -o {{bin}}/{{app}}-{{version}}-darwin-$arch . ;; 62 + GOOS=darwin GOARCH=$arch go build -o {{bin}}/{{app}}-{{version}}-darwin-$arch ./{{pkg}} ;; 62 63 linux) 63 64 CGO_ENABLED=1 CC="zig cc -target ${za}-linux-gnu" \ 64 - GOOS=linux GOARCH=$arch go build -o {{bin}}/{{app}}-{{version}}-linux-$arch . ;; 65 + GOOS=linux GOARCH=$arch go build -o {{bin}}/{{app}}-{{version}}-linux-$arch ./{{pkg}} ;; 65 66 windows) 66 - cp -f packaging/windows/restray_windows_${arch}.syso . 2>/dev/null || true 67 + cp -f packaging/windows/restray_windows_${arch}.syso {{pkg}}/ 2>/dev/null || true 67 68 CGO_ENABLED=1 CC="zig cc -target ${za}-windows-gnu" \ 68 - GOOS=windows GOARCH=$arch go build -ldflags -H=windowsgui -o {{bin}}/{{app}}-{{version}}-windows-$arch.exe . 69 - rm -f restray_windows_${arch}.syso ;; 69 + GOOS=windows GOARCH=$arch go build -ldflags -H=windowsgui -o {{bin}}/{{app}}-{{version}}-windows-$arch.exe ./{{pkg}} 70 + rm -f {{pkg}}/restray_windows_${arch}.syso ;; 70 71 esac 71 72 } 72 73 build_os() { ··· 131 132 just build && {{bin}}/{{app}}-{{version}}-$(go env GOOS)-$(go env GOARCH) 132 133 133 134 lint: 134 - goimports -w . 135 - go vet . 136 - staticcheck . 135 + goimports -w {{pkg}} 136 + go vet ./{{pkg}} 137 + staticcheck ./{{pkg}} 137 138 138 139 bump new: 139 140 #!/usr/bin/env bash 140 141 set -euo pipefail 141 142 [[ "{{new}}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo "Usage: just bump X.Y.Z"; exit 1; } 142 - perl -pi -e 's/^version := ".*"/version := "{{new}}"/' justfile 143 - perl -pi -e 's/^const version = ".*"/const version = "{{new}}"/' main.go 143 + perl -pi -e 's/^const version = ".*"/const version = "{{new}}"/' {{pkg}}/main.go 144 144 perl -pi -e 's/version = ".*"/version = "{{new}}"/' flake.nix 145 145 perl -pi -e 's/!define VERSION ".*"/!define VERSION "{{new}}"/' packaging/windows/installer.nsi 146 146 perl -pi -e 's|(<string>)[^<]*(</string>)|${1}{{new}}${2}| if $prev =~ /CFBundle(Version|ShortVersionString)/; $prev = $_' packaging/darwin/Info.plist 147 147 echo "Bumped to {{new}}" 148 - grep -n "{{new}}" justfile main.go flake.nix packaging/windows/installer.nsi packaging/darwin/Info.plist 148 + grep -n "{{new}}" {{pkg}}/main.go flake.nix packaging/windows/installer.nsi packaging/darwin/Info.plist 149 149 150 150 clean: 151 151 rm -rf {{bin}} {{dist}} result
+3
main.go cmd/restray/main.go
··· 5 5 "os" 6 6 7 7 "fyne.io/systray" 8 + "github.com/gen2brain/beeep" 8 9 ) 9 10 10 11 const version = "0.6.1" ··· 24 25 log.SetFlags(log.Ldate | log.Ltime) 25 26 } 26 27 28 + beeep.AppName = "Restray" 27 29 log.Printf("Restray v%s", version) 28 30 31 + initAlertIcon() 29 32 systray.Run(onReady, func() {}) 30 33 }
+18 -229
restic.go cmd/restray/operations.go
··· 2 2 3 3 import ( 4 4 "bufio" 5 - "context" 6 5 "encoding/json" 7 6 "fmt" 8 7 "io" ··· 19 18 "github.com/gen2brain/beeep" 20 19 ) 21 20 22 - // optional restic path baked at build time for Nix and other pkg managers 23 - var resticBuiltinPath string 24 - 25 - var ( 26 - resticOnce sync.Once 27 - resticPath string 28 - resticManaged bool 29 - ) 30 - 31 - func findRestic() (path string, managed bool) { 32 - resticOnce.Do(func() { 33 - if p, err := exec.LookPath("restic"); err == nil { 34 - resticPath = p 35 - return 36 - } 37 - if p := findResticFromShell(); p != "" { 38 - resticPath = p 39 - return 40 - } 41 - if resticBuiltinPath != "" { 42 - if _, err := os.Stat(resticBuiltinPath); err == nil { 43 - resticPath = resticBuiltinPath 44 - return 45 - } 46 - } 47 - if p := managedRestic(); p != "" { 48 - resticPath = p 49 - resticManaged = true 50 - } 51 - }) 52 - return resticPath, resticManaged 53 - } 54 - 55 - func findResticFromShell() string { 56 - if runtime.GOOS == "windows" { 57 - return "" 58 - } 59 - shell := os.Getenv("SHELL") 60 - if shell == "" { 61 - if runtime.GOOS == "darwin" { 62 - shell = "/bin/zsh" 63 - } else { 64 - shell = "/bin/sh" 65 - } 66 - } 67 - out, err := exec.Command(shell, "-l", "-c", "which restic").Output() 68 - if err == nil { 69 - if p := strings.TrimSpace(string(out)); p != "" { 70 - if _, err := os.Stat(p); err == nil { 71 - return p 72 - } 73 - } 74 - } 75 - for _, dir := range []string{ 76 - "/opt/homebrew/bin", 77 - "/usr/local/bin", 78 - filepath.Join(os.Getenv("HOME"), ".nix-profile/bin"), 79 - "/run/current-system/sw/bin", 80 - filepath.Join(os.Getenv("HOME"), ".local/bin"), 81 - } { 82 - p := filepath.Join(dir, "restic") 83 - if _, err := os.Stat(p); err == nil { 84 - return p 85 - } 86 - } 87 - return "" 88 - } 89 - 90 - func resticEnv(prof Profile) []string { 91 - env := os.Environ() 92 - vars, err := parseEnvFile(prof.EnvFile) 93 - if err != nil { 94 - return env 95 - } 96 - for k, v := range vars { 97 - env = append(env, k+"="+v) 98 - } 99 - return env 100 - } 101 - 102 - func resticCmd(prof Profile, args ...string) *exec.Cmd { 103 - path, _ := findRestic() 104 - log.Printf("[%s] run: restic %s", prof.displayName(), strings.Join(args, " ")) 105 - cmd := exec.Command(path, args...) 106 - cmd.Env = resticEnv(prof) 107 - hideWindow(cmd) 108 - return cmd 109 - } 110 - 111 - type repoResult struct { 112 - errMsg string 113 - needsInit bool 114 - } 115 - 116 - func repoStatus(prof Profile) repoResult { 117 - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 118 - defer cancel() 119 - path, _ := findRestic() 120 - cmd := exec.CommandContext(ctx, path, "cat", "config", "--no-lock", "-q") 121 - cmd.Env = resticEnv(prof) 122 - hideWindow(cmd) 123 - out, err := cmd.CombinedOutput() 124 - if err != nil { 125 - if ctx.Err() != nil { 126 - return repoResult{errMsg: "Repository unreachable"} 127 - } 128 - lines := strings.Split(strings.TrimSpace(string(out)), "\n") 129 - msg, code := classifyError(err, lines[len(lines)-1]) 130 - return repoResult{errMsg: msg, needsInit: code == 10} 131 - } 132 - return repoResult{} 133 - } 134 - 135 - type appState struct { 136 - mu sync.Mutex 137 - busyProfiles map[int]*exec.Cmd 138 - onBusyChanged func(bool) 139 - mountCmds map[int]*exec.Cmd 140 - mountStopping map[int]bool 141 - notifications string 142 - failStatus map[int]string 143 - } 144 - 145 - var state = appState{ 146 - busyProfiles: make(map[int]*exec.Cmd), 147 - mountCmds: make(map[int]*exec.Cmd), 148 - mountStopping: make(map[int]bool), 149 - failStatus: make(map[int]string), 150 - } 151 - 152 - func isProfileBusy(idx int) bool { 153 - state.mu.Lock() 154 - defer state.mu.Unlock() 155 - _, ok := state.busyProfiles[idx] 156 - return ok 157 - } 158 - 159 - func isAnyBusy() bool { 160 - state.mu.Lock() 161 - defer state.mu.Unlock() 162 - return len(state.busyProfiles) > 0 163 - } 164 - 165 - func setProfileBusy(idx int, active bool) { 166 - state.mu.Lock() 167 - applyBusy(idx, active) 168 - } 169 - 170 - func acquireProfile(idx int) bool { 171 - state.mu.Lock() 172 - if _, busy := state.busyProfiles[idx]; busy { 173 - state.mu.Unlock() 174 - return false 175 - } 176 - applyBusy(idx, true) 177 - return true 178 - } 179 - 180 - func applyBusy(idx int, active bool) { 181 - wasBusy := len(state.busyProfiles) > 0 182 - if active { 183 - state.busyProfiles[idx] = nil 184 - } else { 185 - delete(state.busyProfiles, idx) 186 - } 187 - nowBusy := len(state.busyProfiles) > 0 188 - if nowBusy && !wasBusy { 189 - setIconAnimated("busy") 190 - } 191 - cb := state.onBusyChanged 192 - state.mu.Unlock() 193 - if cb != nil { 194 - cb(nowBusy) 195 - } 196 - } 197 - 198 - func setProfileBusyCmd(idx int, cmd *exec.Cmd) { 199 - state.mu.Lock() 200 - defer state.mu.Unlock() 201 - state.busyProfiles[idx] = cmd 202 - } 203 - 204 - func cancelProfile(idx int) { 205 - state.mu.Lock() 206 - defer state.mu.Unlock() 207 - if cmd := state.busyProfiles[idx]; cmd != nil && cmd.Process != nil { 208 - interruptProcess(cmd.Process) 209 - } 210 - } 211 - 212 21 type stderrLogger struct { 213 22 mu sync.Mutex 214 23 last string ··· 236 45 } 237 46 }() 238 47 return sl 239 - } 240 - 241 - func exitCode(err error) int { 242 - if exitErr, ok := err.(*exec.ExitError); ok { 243 - return exitErr.ExitCode() 244 - } 245 - return -1 246 - } 247 - 248 - func killedBySystem(err error) bool { 249 - return err != nil && exitCode(err) == -1 && strings.Contains(err.Error(), "killed") 250 - } 251 - 252 - func classifyError(err error, lastStderr string) (string, int) { 253 - code := exitCode(err) 254 - if killedBySystem(err) { 255 - return "Process terminated by system", code 256 - } 257 - if code == 12 { 258 - return "Wrong password", code 259 - } 260 - if lastStderr != "" { 261 - var msg struct { 262 - Message string `json:"message"` 263 - } 264 - if json.Unmarshal([]byte(lastStderr), &msg) == nil && msg.Message != "" { 265 - return msg.Message, code 266 - } 267 - return lastStderr, code 268 - } 269 - if err != nil { 270 - return err.Error(), code 271 - } 272 - return "", code 273 48 } 274 49 275 50 func runResticOnce(idx int, prof Profile, mStatus prefixedMenuItem, args ...string) (string, error) { ··· 666 441 } 667 442 } 668 443 669 - func init() { 670 - beeep.AppName = "Restray" 671 - } 672 - 673 444 func notifyMode() string { 674 445 state.mu.Lock() 675 446 defer state.mu.Unlock() ··· 690 461 } 691 462 beeep.Notify(operation+" complete", operation+" completed successfully", "") 692 463 } 464 + 465 + func openConsole(prof Profile, profileCount int, terminal, tailFile string) { 466 + vars, err := parseEnvFile(prof.EnvFile) 467 + if err != nil { 468 + log.Printf("console: failed to read env file: %v", err) 469 + return 470 + } 471 + 472 + consoleMsg := "Restray: environment loaded in shell. Run \"restic\" for commands." 473 + if profileCount > 1 { 474 + consoleMsg = fmt.Sprintf("Restray: environment for profile \"%s\" loaded in shell. Run \"restic\" for commands.", prof.displayName()) 475 + } 476 + 477 + cmd := consoleCmd(vars, consoleMsg, "Restray: tailing log.", tailFile, terminal) 478 + if cmd != nil { 479 + cmd.Start() 480 + } 481 + }
restic_manage_other.go cmd/restray/manage_other.go
restic_manage_windows.go cmd/restray/manage_windows.go
templates/config.toml cmd/restray/templates/config.toml
templates/default.env cmd/restray/templates/default.env
tray.go cmd/restray/tray.go
update.go cmd/restray/update.go