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: rewrite cli with urfave/cli

intergrav (Jul 1, 2026, 6:24 AM EDT) 785cd74d d5634aa9

+180 -225
+1
README.md
··· 141 141 - [github.com/robfig/cron](https://github.com/robfig/cron) - cron scheduler 142 142 - [github.com/lnquy/cron](https://github.com/lnquy/cron) - convert cron to human-readable expression 143 143 - [github.com/thiagokokada/dark-mode-go](https://github.com/thiagokokada/dark-mode-go) - dark mode detection for icons 144 + - [github.com/urfave/cli](https://github.com/urfave/cli) - cli library
+129 -191
cmd/restray/cli.go
··· 1 1 package main 2 2 3 3 import ( 4 + "context" 4 5 "errors" 5 - "flag" 6 6 "fmt" 7 7 "os" 8 8 "os/exec" ··· 14 14 "syscall" 15 15 16 16 "github.com/robfig/cron/v3" 17 + "github.com/urfave/cli/v3" 17 18 ) 18 19 19 - func cliUsage() { 20 - fmt.Fprintf(os.Stderr, `Usage: restray [command] 21 - 22 - Commands: 23 - (no command) Launch restray (GUI + built-in scheduler) 24 - daemon Launch restray (built-in scheduler only) 25 - schedule [-p name] Run full schedule with hooks 26 - operation backup [-p name] Run backup 27 - operation prune [-p name] Prune old snapshots 28 - operation check [-p name] Verify repository integrity 29 - operation unlock [-p name] Remove stale locks 30 - operation shell [-p name] Open shell with restic environment 31 - operation mount [-p name] Mount repository (Ctrl+C to unmount) 32 - operation pre-hook [-p name] Run pre-hook command 33 - operation post-hook [-p name] Run post-hook command 34 - configure config Create/open config file in editor 35 - configure env [-p name] Create/open env file in editor 36 - 37 - Flags: 38 - -c, --config dir Config directory (default: $RESTRAY_CONFIG or OS default) 39 - -p, --profile name Profile name (default: first profile) 40 - `) 41 - } 42 - 43 20 func resolveProfile(cfg Config, name string) (int, Profile, error) { 44 21 if name == "" { 45 22 return 0, cfg.Profiles[0], nil ··· 56 33 return 0, Profile{}, fmt.Errorf("profile %q not found (available: %s)", name, strings.Join(names, ", ")) 57 34 } 58 35 59 - func cliParseFlags(args []string) (profileName string, remaining []string) { 60 - fs := flag.NewFlagSet("restray", flag.ContinueOnError) 61 - fs.StringVar(&profileName, "p", "", "profile name") 62 - fs.StringVar(&profileName, "profile", "", "profile name") 63 - fs.Parse(args) 64 - return profileName, fs.Args() 65 - } 66 - 67 36 func editorCmd() string { 68 37 if e := os.Getenv("VISUAL"); e != "" { 69 38 return e ··· 77 46 return "vi" 78 47 } 79 48 80 - func cliEdit(path string) int { 49 + func cliEdit(path string) error { 81 50 cmd := exec.Command(editorCmd(), path) 82 51 cmd.Stdin = os.Stdin 83 52 cmd.Stdout = os.Stdout 84 53 cmd.Stderr = os.Stderr 85 54 if err := cmd.Run(); err != nil { 86 - if code := exitCode(err); code > 0 { 87 - return code 88 - } 89 - return 1 90 - } 91 - return 0 92 - } 93 - 94 - func cliConfigure(args []string) int { 95 - if len(args) == 0 { 96 - fmt.Fprintln(os.Stderr, `Usage: restray configure <config|env> [-p name]`) 97 - return 1 98 - } 99 - target := args[0] 100 - name, _ := cliParseFlags(args[1:]) 101 - switch target { 102 - case "config": 103 - ensureConfigFile() 104 - return cliEdit(configPath()) 105 - case "env": 106 - cfg := loadConfig() 107 - _, prof, err := resolveProfile(cfg, name) 108 - if err != nil { 109 - fmt.Fprintln(os.Stderr, "error:", err) 110 - return 1 111 - } 112 - ensureEnvFile(prof.EnvFile) 113 - return cliEdit(prof.EnvFile) 114 - default: 115 - fmt.Fprintf(os.Stderr, "unknown configure target: %s\n", target) 116 - return 1 55 + return cli.Exit("", exitCode(err)) 117 56 } 57 + return nil 118 58 } 119 59 120 - func cliRunRestic(prof Profile, args ...string) int { 60 + func cliRunRestic(prof Profile, args ...string) error { 121 61 p, _ := findRestic() 122 62 if p == "" { 123 - fmt.Fprintln(os.Stderr, "error: restic not found") 124 - return 1 63 + return cli.Exit("error: restic not found", 1) 125 64 } 126 65 cmd := resticCmd(prof, args...) 127 66 cmd.Stdout = os.Stdout 128 67 cmd.Stderr = os.Stderr 129 68 if err := cmd.Run(); err != nil { 130 - if code := exitCode(err); code > 0 { 131 - return code 132 - } 133 - return 1 69 + return cli.Exit("", exitCode(err)) 134 70 } 135 - return 0 71 + return nil 136 72 } 137 73 138 - func cliRunHook(hook string, prof Profile, extraEnv ...string) int { 74 + func cliRunHook(hook string, prof Profile, extraEnv ...string) error { 139 75 if hook == "" { 140 - fmt.Fprintln(os.Stderr, "error: no hook configured") 141 - return 1 76 + return cli.Exit("error: no hook configured", 1) 142 77 } 143 78 cmd := hookCmd(hook, prof, extraEnv...) 144 79 cmd.Stdout = os.Stdout 145 80 cmd.Stderr = os.Stderr 146 81 if err := cmd.Run(); err != nil { 147 - if code := exitCode(err); code > 0 { 148 - return code 149 - } 150 - return 1 82 + return cli.Exit("", exitCode(err)) 151 83 } 152 - return 0 84 + return nil 153 85 } 154 86 155 - func cliBackup(prof Profile, scheduled bool) int { 156 - args := []string{"backup"} 157 - if retryLockEnabled(prof) { 158 - args = append(args, "--retry-lock", prof.RetryLock) 159 - } 160 - args = append(args, prof.Backup.Args...) 161 - if scheduled { 162 - args = append(args, prof.Backup.ArgsScheduled...) 163 - } 164 - args = append(args, prof.Backup.Paths...) 165 - return cliRunRestic(prof, args...) 87 + func cliBackup(prof Profile, scheduled bool) error { 88 + return cliRunRestic(prof, backupArgs(prof, scheduled)...) 166 89 } 167 90 168 - func cliPrune(prof Profile) int { 91 + func cliPrune(prof Profile) error { 169 92 return cliRunRestic(prof, forgetArgs(prof)...) 170 93 } 171 94 172 - func cliCheck(prof Profile) int { 95 + func cliCheck(prof Profile) error { 173 96 return cliRunRestic(prof, checkArgs(prof)...) 174 97 } 175 98 176 - func cliUnlock(prof Profile) int { 99 + func cliUnlock(prof Profile) error { 177 100 return cliRunRestic(prof, "unlock") 178 101 } 179 102 180 - func cliShell(prof Profile) int { 103 + func cliPreHook(prof Profile) error { 104 + return cliRunHook(prof.PreHook, prof) 105 + } 106 + 107 + func cliPostHook(prof Profile) error { 108 + return cliRunHook(prof.PostHook, prof) 109 + } 110 + 111 + func cliShell(prof Profile) error { 181 112 shell := os.Getenv("SHELL") 182 113 if shell == "" { 183 114 if runtime.GOOS == "windows" { ··· 198 129 cmd.Stdout = os.Stdout 199 130 cmd.Stderr = os.Stderr 200 131 if err := cmd.Run(); err != nil { 201 - if code := exitCode(err); code > 0 { 202 - return code 203 - } 204 - return 1 132 + return cli.Exit("", exitCode(err)) 205 133 } 206 - return 0 134 + return nil 207 135 } 208 136 209 - func cliMount(prof Profile) int { 137 + func cliMount(prof Profile) error { 210 138 if runtime.GOOS == "windows" { 211 - fmt.Fprintln(os.Stderr, "error: mount is not supported on Windows") 212 - return 1 139 + return cli.Exit("error: mount is not supported on Windows", 1) 213 140 } 214 141 dir, err := os.MkdirTemp("", "restray-mount-") 215 142 if err != nil { 216 - fmt.Fprintf(os.Stderr, "error: %v\n", err) 217 - return 1 143 + return cli.Exit(fmt.Sprintf("error: %v", err), 1) 218 144 } 219 145 defer os.Remove(dir) 220 146 ··· 224 150 return cliRunRestic(prof, args...) 225 151 } 226 152 227 - func cliSchedule(prof Profile) int { 153 + func cliSchedule(prof Profile) error { 228 154 p, _ := findRestic() 229 155 if p == "" { 230 - fmt.Fprintln(os.Stderr, "error: restic not found") 231 - return 1 156 + return cli.Exit("error: restic not found", 1) 232 157 } 233 158 234 159 var interrupted atomic.Bool ··· 258 183 259 184 if prof.PreHook != "" { 260 185 fmt.Fprintln(os.Stderr, "Running pre-hook...") 261 - if code := cliRunHook(prof.PreHook, prof, hookEnv...); code != 0 { 262 - return 1 186 + if err := cliRunHook(prof.PreHook, prof, hookEnv...); err != nil { 187 + return cli.Exit("error: pre-hook failed", 1) 263 188 } 264 189 } 265 190 ··· 267 192 268 193 if !interrupted.Load() && prof.Schedule.BackupEnabled() { 269 194 fmt.Fprintln(os.Stderr, "Running backup...") 270 - if code := cliBackup(prof, true); code != 0 { 195 + if err := cliBackup(prof, true); err != nil { 271 196 failed = true 272 197 } 273 198 } 274 199 275 200 if !interrupted.Load() && prof.Schedule.Prune && len(prof.Prune.Args) > 0 { 276 201 fmt.Fprintln(os.Stderr, "Running prune...") 277 - if code := cliPrune(prof); code != 0 { 202 + if err := cliPrune(prof); err != nil { 278 203 failed = true 279 204 } 280 205 } 281 206 282 207 if !interrupted.Load() && prof.Schedule.Check { 283 208 fmt.Fprintln(os.Stderr, "Running check...") 284 - if code := cliCheck(prof); code != 0 { 209 + if err := cliCheck(prof); err != nil { 285 210 failed = true 286 211 } 287 212 } ··· 297 222 } 298 223 postEnv = append(postEnv, "RESTRAY_ERROR="+errMsg) 299 224 } 300 - if code := cliRunHook(prof.PostHook, prof, postEnv...); code != 0 { 301 - return 1 225 + if err := cliRunHook(prof.PostHook, prof, postEnv...); err != nil { 226 + return cli.Exit("error: post-hook failed", 1) 302 227 } 303 228 } 304 229 305 230 if failed || interrupted.Load() { 306 - return 1 231 + return cli.Exit("error: schedule failed", 1) 307 232 } 308 - return 0 233 + return nil 309 234 } 310 235 311 236 func buildDaemonSchedule(wg *sync.WaitGroup) (*cron.Cron, error) { ··· 348 273 return sched, nil 349 274 } 350 275 351 - func cliDaemon() int { 276 + func cliDaemon(context.Context, *cli.Command) error { 352 277 p, _ := findRestic() 353 278 if p == "" { 354 - fmt.Fprintln(os.Stderr, "error: restic not found") 355 - return 1 279 + return cli.Exit("error: restic not found", 1) 356 280 } 357 281 358 282 var wg sync.WaitGroup 359 283 sched, err := buildDaemonSchedule(&wg) 360 284 if err != nil { 361 - fmt.Fprintln(os.Stderr, "error:", err) 362 - return 1 285 + return cli.Exit(fmt.Sprintf("error: %v", err), 1) 363 286 } 364 287 365 288 sigCh := make(chan os.Signal, 1) ··· 385 308 <-sched.Stop().Done() 386 309 fmt.Fprintln(os.Stderr, "Waiting for running jobs to finish...") 387 310 wg.Wait() 388 - return 0 389 - } 390 - 391 - func cliResolve(args []string) (Profile, error) { 392 - name, _ := cliParseFlags(args) 393 - cfg := loadConfig() 394 - _, prof, err := resolveProfile(cfg, name) 395 - return prof, err 311 + return nil 396 312 } 397 313 398 - func runCLI(args []string) int { 399 - if len(args) == 0 { 400 - cliUsage() 401 - return 1 314 + func profileFlag() *cli.StringFlag { 315 + return &cli.StringFlag{ 316 + Name: "profile", 317 + Aliases: []string{"p"}, 318 + Usage: "profile name (default: first profile)", 402 319 } 403 - 404 - switch args[0] { 405 - case "daemon": 406 - return cliDaemon() 407 - 408 - case "schedule": 409 - prof, err := cliResolve(args[1:]) 410 - if err != nil { 411 - fmt.Fprintln(os.Stderr, "error:", err) 412 - return 1 413 - } 414 - return cliSchedule(prof) 320 + } 415 321 416 - case "op", "operation": 417 - if len(args) < 2 { 418 - cliUsage() 419 - return 1 420 - } 421 - op := args[1] 422 - prof, err := cliResolve(args[2:]) 322 + func profileAction(fn func(Profile) error) cli.ActionFunc { 323 + return func(_ context.Context, cmd *cli.Command) error { 324 + _, prof, err := resolveProfile(loadConfig(), cmd.String("profile")) 423 325 if err != nil { 424 - fmt.Fprintln(os.Stderr, "error:", err) 425 - return 1 426 - } 427 - switch op { 428 - case "backup": 429 - return cliBackup(prof, false) 430 - case "prune": 431 - return cliPrune(prof) 432 - case "check": 433 - return cliCheck(prof) 434 - case "unlock": 435 - return cliUnlock(prof) 436 - case "shell": 437 - return cliShell(prof) 438 - case "mount": 439 - return cliMount(prof) 440 - case "pre-hook": 441 - return cliRunHook(prof.PreHook, prof) 442 - case "post-hook": 443 - return cliRunHook(prof.PostHook, prof) 444 - default: 445 - fmt.Fprintf(os.Stderr, "unknown operation: %s\n", op) 446 - cliUsage() 447 - return 1 326 + return cli.Exit(fmt.Sprintf("error: %v", err), 1) 448 327 } 449 - 450 - case "configure": 451 - return cliConfigure(args[1:]) 328 + return fn(prof) 329 + } 330 + } 452 331 453 - case "-h", "--help", "help": 454 - cliUsage() 455 - return 0 332 + func buildCLI() *cli.Command { 333 + cli.VersionPrinter = func(cmd *cli.Command) { 334 + fmt.Fprintf(cmd.Root().Writer, "restray %s compiled with %s on %s/%s\n", cmd.Version, runtime.Version(), runtime.GOOS, runtime.GOARCH) 335 + } 336 + return &cli.Command{ 337 + Name: "restray", 338 + Usage: "Restic backup scheduler and tray app", 339 + Version: version, 340 + EnableShellCompletion: true, 341 + Flags: []cli.Flag{profileFlag()}, 342 + Commands: []*cli.Command{ 343 + { 344 + Name: "daemon", 345 + Usage: "Launch restray (built-in scheduler only)", 346 + Action: cliDaemon, 347 + }, 348 + { 349 + Name: "schedule", 350 + Usage: "Run full schedule with hooks", 351 + Action: profileAction(cliSchedule), 352 + }, 353 + { 354 + Name: "operation", 355 + Aliases: []string{"op"}, 356 + Usage: "Run a restray operation", 357 + Commands: []*cli.Command{ 358 + {Name: "backup", Usage: "Run backup", Action: profileAction(func(p Profile) error { return cliBackup(p, false) })}, 359 + {Name: "prune", Usage: "Prune old snapshots", Action: profileAction(cliPrune)}, 360 + {Name: "check", Usage: "Verify repository integrity", Action: profileAction(cliCheck)}, 361 + {Name: "unlock", Usage: "Remove stale locks", Action: profileAction(cliUnlock)}, 362 + {Name: "shell", Usage: "Open shell with restic environment", Action: profileAction(cliShell)}, 363 + {Name: "mount", Usage: "Mount repository (Ctrl+C to unmount)", Action: profileAction(cliMount)}, 364 + {Name: "pre-hook", Usage: "Run pre-hook command", Action: profileAction(cliPreHook)}, 365 + {Name: "post-hook", Usage: "Run post-hook command", Action: profileAction(cliPostHook)}, 366 + }, 367 + }, 368 + { 369 + Name: "configure", 370 + Aliases: []string{"co"}, 371 + Usage: "Create/open config or env files", 372 + Commands: []*cli.Command{ 373 + { 374 + Name: "config", 375 + Usage: "Create/open config file in editor", 376 + Action: func(context.Context, *cli.Command) error { 377 + ensureConfigFile() 378 + return cliEdit(configPath()) 379 + }, 380 + }, 381 + { 382 + Name: "env", 383 + Usage: "Create/open env file in editor", 384 + Action: profileAction(func(p Profile) error { 385 + ensureEnvFile(p.EnvFile) 386 + return cliEdit(p.EnvFile) 387 + }), 388 + }, 389 + }, 390 + }, 391 + }, 392 + } 393 + } 456 394 457 - case "--version", "version": 458 - fmt.Printf("restray %s compiled with %s on %s/%s\n", version, runtime.Version(), runtime.GOOS, runtime.GOARCH) 459 - return 0 395 + func runCLI(args []string) int { 396 + exitCodeResult := 0 397 + cli.OsExiter = func(code int) { exitCodeResult = code } 460 398 461 - default: 462 - fmt.Fprintf(os.Stderr, "unknown command: %s\n", args[0]) 463 - cliUsage() 464 - return 1 399 + fullArgs := append([]string{"restray"}, args...) 400 + if err := buildCLI().Run(context.Background(), fullArgs); err != nil && exitCodeResult == 0 { 401 + exitCodeResult = 1 465 402 } 403 + return exitCodeResult 466 404 }
+14 -9
cmd/restray/operations.go
··· 176 176 } 177 177 178 178 func doBackup(idx int, mStatus prefixedMenuItem, prof Profile, scheduled bool) (bool, int) { 179 - args := []string{"backup", "--json"} 180 - if retryLockEnabled(prof) { 181 - args = append(args, "--retry-lock", prof.RetryLock) 182 - } 183 - args = append(args, prof.Backup.Args...) 184 - if scheduled { 185 - args = append(args, prof.Backup.ArgsScheduled...) 186 - } 187 - args = append(args, prof.Backup.Paths...) 179 + args := backupArgs(prof, scheduled, "--json") 188 180 189 181 ok, code := doBackupOnce(idx, mStatus, prof, args) 190 182 if !ok && code == 11 && retryLockEnabled(prof) { ··· 350 342 } 351 343 return !failed, "" 352 344 }) 345 + } 346 + 347 + func backupArgs(prof Profile, scheduled bool, extra ...string) []string { 348 + args := append([]string{"backup"}, extra...) 349 + if retryLockEnabled(prof) { 350 + args = append(args, "--retry-lock", prof.RetryLock) 351 + } 352 + args = append(args, prof.Backup.Args...) 353 + if scheduled { 354 + args = append(args, prof.Backup.ArgsScheduled...) 355 + } 356 + args = append(args, prof.Backup.Paths...) 357 + return args 353 358 } 354 359 355 360 func forgetArgs(prof Profile) []string {
+31 -24
flake.nix
··· 25 25 pname = "restray"; 26 26 version = "0.15.0"; 27 27 src = ./.; 28 - vendorHash = "sha256-ozeoWgJ/e7zAZqUzmFFCV4VXnsINGJ55pfWzjqJkFUM="; 28 + vendorHash = "sha256-RUcFB8BLPDFCvs2ynMejkxXJDLM3CCLhDs4OxopGAag="; 29 29 subPackages = ["cmd/restray"]; 30 30 31 31 ldflags = ["-X" "main.resticBuiltinPath=${pkgs.restic}/bin/restic"]; ··· 35 35 CGO_LDFLAGS = "-mmacosx-version-min=11.0"; 36 36 }; 37 37 38 - nativeBuildInputs = pkgs.lib.optionals (!isDarwin) [pkgs.pkg-config]; 38 + nativeBuildInputs = pkgs.lib.optionals (!isDarwin) [pkgs.pkg-config] ++ [pkgs.installShellFiles]; 39 39 buildInputs = pkgs.lib.optionals (!isDarwin) [pkgs.glib]; 40 40 41 - postInstall = 42 - if isDarwin 43 - then '' 44 - mkdir -p $out/Applications/Restray.app/Contents/{MacOS,Resources} 45 - mv $out/bin/restray $out/Applications/Restray.app/Contents/MacOS/Restray 46 - cp packaging/darwin/Info.plist $out/Applications/Restray.app/Contents/Info.plist 47 - cp packaging/darwin/restray.icns $out/Applications/Restray.app/Contents/Resources/restray.icns 48 - mkdir -p $out/bin 49 - ln -s $out/Applications/Restray.app/Contents/MacOS/Restray $out/bin/restray 50 - '' 51 - else '' 52 - install -Dm644 packaging/linux/restray.desktop $out/share/applications/restray.desktop 53 - install -Dm644 packaging/linux/restray.png $out/share/icons/hicolor/256x256/apps/restray.png 54 - substituteInPlace $out/share/applications/restray.desktop \ 55 - --replace-fail "Exec=restray" "Exec=$out/bin/restray" 56 - install -Dm644 packaging/linux/restray-user.service $out/lib/systemd/user/restray.service 57 - substituteInPlace $out/lib/systemd/user/restray.service \ 58 - --replace-fail "ExecStart=restray daemon" "ExecStart=$out/bin/restray daemon" 59 - install -Dm644 packaging/linux/restray.service $out/lib/systemd/system/restray.service 60 - substituteInPlace $out/lib/systemd/system/restray.service \ 61 - --replace-fail "ExecStart=restray daemon" "ExecStart=$out/bin/restray daemon" 62 - ''; 41 + postInstall = '' 42 + ${ 43 + if isDarwin 44 + then '' 45 + mkdir -p $out/Applications/Restray.app/Contents/{MacOS,Resources} 46 + mv $out/bin/restray $out/Applications/Restray.app/Contents/MacOS/Restray 47 + cp packaging/darwin/Info.plist $out/Applications/Restray.app/Contents/Info.plist 48 + cp packaging/darwin/restray.icns $out/Applications/Restray.app/Contents/Resources/restray.icns 49 + mkdir -p $out/bin 50 + ln -s $out/Applications/Restray.app/Contents/MacOS/Restray $out/bin/restray 51 + '' 52 + else '' 53 + install -Dm644 packaging/linux/restray.desktop $out/share/applications/restray.desktop 54 + install -Dm644 packaging/linux/restray.png $out/share/icons/hicolor/256x256/apps/restray.png 55 + substituteInPlace $out/share/applications/restray.desktop \ 56 + --replace-fail "Exec=restray" "Exec=$out/bin/restray" 57 + install -Dm644 packaging/linux/restray-user.service $out/lib/systemd/user/restray.service 58 + substituteInPlace $out/lib/systemd/user/restray.service \ 59 + --replace-fail "ExecStart=restray daemon" "ExecStart=$out/bin/restray daemon" 60 + install -Dm644 packaging/linux/restray.service $out/lib/systemd/system/restray.service 61 + substituteInPlace $out/lib/systemd/system/restray.service \ 62 + --replace-fail "ExecStart=restray daemon" "ExecStart=$out/bin/restray daemon" 63 + '' 64 + } 65 + installShellCompletion --cmd restray \ 66 + --bash <($out/bin/restray completion bash) \ 67 + --zsh <($out/bin/restray completion zsh) \ 68 + --fish <($out/bin/restray completion fish) 69 + ''; 63 70 64 71 meta = { 65 72 description = "Restic backup scheduler system tray app";
+1
go.mod
··· 11 11 github.com/lnquy/cron v1.1.1 12 12 github.com/robfig/cron/v3 v3.0.1 13 13 github.com/thiagokokada/dark-mode-go v0.0.2 14 + github.com/urfave/cli/v3 v3.10.1 14 15 ) 15 16 16 17 require (
+4 -1
go.sum
··· 40 40 github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 41 41 github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 42 42 github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 43 - github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 44 43 github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 44 + github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= 45 + github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 45 46 github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af h1:6yITBqGTE2lEeTPG04SN9W+iWHCRyHqlVYILiSXziwk= 46 47 github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af/go.mod h1:4F09kP5F+am0jAwlQLddpoMDM+iewkxxt6nxUQ5nq5o= 47 48 github.com/thiagokokada/dark-mode-go v0.0.2 h1:e4TAW4A/VylmlFKL9Jo83xYlu7rqWl+keH6lQO5owio= 48 49 github.com/thiagokokada/dark-mode-go v0.0.2/go.mod h1:IQrRBLMIz8vfFZ/sdZr7ASfW1SpE9TJwUCDbKbG2AQU= 50 + github.com/urfave/cli/v3 v3.10.1 h1:7Kx9H50hrHbRbyxgO1KP6/BcbiGRz0uYh5YyQ30JEEY= 51 + github.com/urfave/cli/v3 v3.10.1/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso= 49 52 golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 50 53 golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= 51 54 golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=