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: nix module config + hm

also migrated "general" config section to "gui" and deprecated "general". i think it makes more sense since the general settings were not used by daemon at all

intergrav (Jun 30, 2026, 5:07 PM EDT) f161f3a2 ecc579a1

+287 -47
+3 -1
README.md
··· 48 48 49 49 ## Configuration 50 50 51 - ### `[general]` 51 + ### `[gui]` 52 + 53 + GUI-only settings, ignored by `restray daemon`. 52 54 53 55 | Key | Type | Default | Description | 54 56 | ------------------ | ------ | --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+13 -5
cmd/restray/config.go
··· 63 63 Mount Mount `toml:"mount"` 64 64 } 65 65 66 - type General struct { 66 + type GUI struct { 67 67 CheckUpdates *bool `toml:"check_updates"` 68 68 ManageRestic bool `toml:"manage_restic"` 69 69 Notifications string `toml:"notifications"` ··· 71 71 Terminal string `toml:"terminal"` 72 72 } 73 73 74 - func (g General) UpdatesEnabled() bool { 74 + func (g GUI) UpdatesEnabled() bool { 75 75 return g.CheckUpdates == nil || *g.CheckUpdates 76 76 } 77 77 78 78 type Config struct { 79 - General General `toml:"general"` 79 + GUI GUI `toml:"gui"` 80 + General GUI `toml:"general"` // deprecated, use [gui] 80 81 Profiles []Profile `toml:"profiles"` 81 82 Profile []Profile `toml:"profile"` 82 83 } ··· 214 215 cfg.Profiles = append(cfg.Profiles, cfg.Profile...) 215 216 cfg.Profile = nil 216 217 } 217 - if cfg.General.ScheduleDisplay == "" { 218 - cfg.General.ScheduleDisplay = "description" 218 + if cfg.General != (GUI{}) { 219 + log.Printf("config: [general] is deprecated, use [gui] instead") 220 + if cfg.GUI == (GUI{}) { 221 + cfg.GUI = cfg.General 222 + } 223 + cfg.General = GUI{} 224 + } 225 + if cfg.GUI.ScheduleDisplay == "" { 226 + cfg.GUI.ScheduleDisplay = "description" 219 227 } 220 228 if len(cfg.Profiles) == 0 { 221 229 cfg.Profiles = append(cfg.Profiles, Profile{Name: "default"})
+1 -1
cmd/restray/templates/config.toml
··· 1 1 # Restray configuration 2 2 # Configuration reference: https://tangled.org/devins.page/restray#configuration 3 3 4 - [general] 4 + [gui] 5 5 # check_updates = true 6 6 # manage_restic = false 7 7 # notifications = "none"
+7 -7
cmd/restray/tray.go
··· 295 295 return prefix + "Paused (battery)" 296 296 } 297 297 next := formatNextFor(sched, eids) 298 - display := cfg.General.ScheduleDisplay 298 + display := cfg.GUI.ScheduleDisplay 299 299 switch display { 300 300 case "none", "hidden": 301 301 if next != "" { ··· 427 427 } 428 428 cfg := loadConfig() 429 429 state.mu.Lock() 430 - state.notifications = strings.ToLower(cfg.General.Notifications) 430 + state.notifications = strings.ToLower(cfg.GUI.Notifications) 431 431 state.mu.Unlock() 432 432 sched.Stop() 433 433 sched = cron.New() ··· 439 439 440 440 resticPath, managed := findRestic() 441 441 442 - if resticPath == "" && selfManagesRestic && cfg.General.ManageRestic { 442 + if resticPath == "" && selfManagesRestic && cfg.GUI.ManageRestic { 443 443 mGlobalStatus.SetTitle("Downloading restic...") 444 444 mGlobalStatus.Show() 445 445 go doDownload() ··· 548 548 applyProfileUI(cfg) 549 549 550 550 if managed { 551 - go checkResticUpdate(resticPath, cfg.General.ManageRestic, mUpdate, mGlobalStatus, applyConfig) 551 + go checkResticUpdate(resticPath, cfg.GUI.ManageRestic, mUpdate, mGlobalStatus, applyConfig) 552 552 } 553 553 } 554 554 ··· 568 568 resticPath, managed := findRestic() 569 569 if managed && !isAnyBusy() { 570 570 cfg := loadConfig() 571 - checkResticUpdate(resticPath, cfg.General.ManageRestic, mUpdate, mGlobalStatus, applyConfig) 571 + checkResticUpdate(resticPath, cfg.GUI.ManageRestic, mUpdate, mGlobalStatus, applyConfig) 572 572 } 573 573 } 574 574 }() ··· 669 669 } 670 670 case <-mConsole.ClickedCh: 671 671 cfg := loadConfig() 672 - openConsole(selectedProfile(), len(cfg.Profiles), cfg.General.Terminal, "") 672 + openConsole(selectedProfile(), len(cfg.Profiles), cfg.GUI.Terminal, "") 673 673 case <-mPreHook.ClickedCh: 674 674 prof := selectedProfile() 675 675 if prof.PreHook != "" { ··· 726 726 } 727 727 case <-mLog.ClickedCh: 728 728 cfg := loadConfig() 729 - openConsole(selectedProfile(), len(cfg.Profiles), cfg.General.Terminal, logPath()) 729 + openConsole(selectedProfile(), len(cfg.Profiles), cfg.GUI.Terminal, logPath()) 730 730 case <-mFolder.ClickedCh: 731 731 openFile(configDir()) 732 732 case <-mAbout.ClickedCh:
+3 -33
flake.nix
··· 11 11 nixpkgs, 12 12 flake-utils, 13 13 }: let 14 - # ./packaging/linux/restray.service 15 - nixosModules.default = { 16 - config, 17 - lib, 18 - pkgs, 19 - ... 20 - }: let 21 - cfg = config.services.restray; 22 - in { 23 - options.services.restray = { 24 - enable = lib.mkEnableOption "Restray backup scheduler daemon"; 25 - package = lib.mkPackageOption self.packages.${pkgs.system} "default" {}; 26 - }; 27 - 28 - config = lib.mkIf cfg.enable { 29 - systemd.services.restray = { 30 - description = "Restray backup scheduler daemon"; 31 - wantedBy = ["multi-user.target"]; 32 - serviceConfig = { 33 - Type = "simple"; 34 - ExecStart = "${lib.getExe cfg.package} daemon"; 35 - ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID"; 36 - Restart = "on-failure"; 37 - StateDirectory = "restray"; 38 - Environment = [ 39 - "RESTRAY_CONFIG=/etc/restray" 40 - "RESTRAY_STATE=/var/lib/restray" 41 - ]; 42 - }; 43 - }; 44 - }; 45 - }; 14 + nixosModules.default = import ./nix/nixos-module.nix self; 15 + homeManagerModules.default = import ./nix/hm-module.nix self; 46 16 in 47 17 { 48 - inherit nixosModules; 18 + inherit nixosModules homeManagerModules; 49 19 } 50 20 // flake-utils.lib.eachDefaultSystem (system: let 51 21 pkgs = nixpkgs.legacyPackages.${system};
+69
nix/hm-module.nix
··· 1 + self: { 2 + config, 3 + lib, 4 + pkgs, 5 + ... 6 + }: let 7 + cfg = config.programs.restray; 8 + daemon = config.services.restray; 9 + shared = import ./shared.nix {inherit lib pkgs;}; 10 + in { 11 + options.programs.restray = { 12 + enable = lib.mkEnableOption "Restray backup scheduler"; 13 + package = lib.mkPackageOption self.packages.${pkgs.system} "default" {}; 14 + 15 + profiles = lib.mkOption { 16 + type = lib.types.attrsOf (lib.types.submodule shared.profileOpts); 17 + default = {}; 18 + description = "Restray backup profiles."; 19 + }; 20 + }; 21 + 22 + options.services.restray.enable = lib.mkEnableOption "Restray backup scheduler daemon"; 23 + 24 + config = lib.mkMerge [ 25 + (lib.mkIf daemon.enable { 26 + programs.restray.enable = lib.mkDefault true; 27 + }) 28 + 29 + (lib.mkIf cfg.enable { 30 + home.packages = [cfg.package]; 31 + xdg.configFile."restray/config.toml".source = shared.configFile cfg.profiles; 32 + }) 33 + 34 + (lib.mkIf (daemon.enable && cfg.enable) { 35 + assertions = [ 36 + { 37 + assertion = cfg.profiles != {}; 38 + message = "services.restray.enable requires at least one programs.restray.profiles.<name> to be configured."; 39 + } 40 + ]; 41 + }) 42 + 43 + (lib.mkIf (daemon.enable && cfg.enable && pkgs.stdenv.isLinux) { 44 + systemd.user.services.restray = { 45 + Unit.Description = "Restray backup scheduler daemon"; 46 + Install.WantedBy = ["default.target"]; 47 + Service = { 48 + Type = "simple"; 49 + ExecStart = "${lib.getExe cfg.package} daemon"; 50 + ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID"; 51 + Restart = "on-failure"; 52 + }; 53 + }; 54 + }) 55 + 56 + (lib.mkIf (daemon.enable && cfg.enable && pkgs.stdenv.isDarwin) { 57 + launchd.agents.restray = { 58 + enable = true; 59 + config = { 60 + ProgramArguments = ["${lib.getExe cfg.package}" "daemon"]; 61 + KeepAlive = true; 62 + RunAtLoad = true; 63 + StandardErrorPath = "${config.home.homeDirectory}/Library/Logs/restray.daemon.log"; 64 + StandardOutPath = "${config.home.homeDirectory}/Library/Logs/restray.daemon.log"; 65 + }; 66 + }; 67 + }) 68 + ]; 69 + }
+47
nix/nixos-module.nix
··· 1 + self: { 2 + config, 3 + lib, 4 + pkgs, 5 + ... 6 + }: let 7 + cfg = config.services.restray; 8 + shared = import ./shared.nix {inherit lib pkgs;}; 9 + in { 10 + options.services.restray = { 11 + enable = lib.mkEnableOption "Restray backup scheduler daemon"; 12 + package = lib.mkPackageOption self.packages.${pkgs.system} "default" {}; 13 + 14 + profiles = lib.mkOption { 15 + type = lib.types.attrsOf (lib.types.submodule shared.profileOpts); 16 + default = {}; 17 + description = "Restray backup profiles."; 18 + }; 19 + }; 20 + 21 + config = lib.mkIf cfg.enable { 22 + assertions = [ 23 + { 24 + assertion = cfg.profiles != {}; 25 + message = "services.restray.enable requires at least one services.restray.profiles.<name> to be configured."; 26 + } 27 + ]; 28 + 29 + systemd.services.restray = { 30 + description = "Restray backup scheduler daemon"; 31 + wantedBy = ["multi-user.target"]; 32 + serviceConfig = { 33 + Type = "simple"; 34 + ExecStart = "${lib.getExe cfg.package} daemon"; 35 + ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID"; 36 + Restart = "on-failure"; 37 + StateDirectory = "restray"; 38 + Environment = [ 39 + "RESTRAY_CONFIG=/etc/restray" 40 + "RESTRAY_STATE=/var/lib/restray" 41 + ]; 42 + }; 43 + }; 44 + 45 + environment.etc."restray/config.toml".source = shared.configFile cfg.profiles; 46 + }; 47 + }
+144
nix/shared.nix
··· 1 + { 2 + lib, 3 + pkgs, 4 + }: let 5 + profileOpts = {name, ...}: { 6 + options = { 7 + name = lib.mkOption { 8 + type = lib.types.str; 9 + default = name; 10 + description = "Profile name."; 11 + }; 12 + envFile = lib.mkOption { 13 + type = lib.types.nullOr lib.types.str; 14 + default = null; 15 + description = "Path to an env file with RESTIC_REPOSITORY, RESTIC_PASSWORD, etc."; 16 + example = "/run/secrets/restray-home-env"; 17 + }; 18 + retryLock = lib.mkOption { 19 + type = lib.types.str; 20 + default = "2m"; 21 + description = "How long to retry acquiring the repository lock."; 22 + }; 23 + preHook = lib.mkOption { 24 + type = lib.types.str; 25 + default = ""; 26 + description = "Command to run before backup/prune/check."; 27 + }; 28 + postHook = lib.mkOption { 29 + type = lib.types.str; 30 + default = ""; 31 + description = "Command to run after backup/prune/check."; 32 + }; 33 + schedule = { 34 + cron = lib.mkOption { 35 + type = lib.types.str; 36 + default = ""; 37 + description = "Cron expression for the backup schedule."; 38 + }; 39 + onBattery = lib.mkOption { 40 + type = lib.types.bool; 41 + default = false; 42 + description = "Run scheduled jobs while on battery power."; 43 + }; 44 + backup = lib.mkOption { 45 + type = lib.types.bool; 46 + default = true; 47 + description = "Run a backup on schedule."; 48 + }; 49 + prune = lib.mkOption { 50 + type = lib.types.bool; 51 + default = false; 52 + description = "Run a prune on schedule."; 53 + }; 54 + check = lib.mkOption { 55 + type = lib.types.bool; 56 + default = false; 57 + description = "Run a check on schedule."; 58 + }; 59 + }; 60 + backup = { 61 + paths = lib.mkOption { 62 + type = lib.types.listOf lib.types.str; 63 + default = []; 64 + description = "Paths to back up."; 65 + }; 66 + args = lib.mkOption { 67 + type = lib.types.listOf lib.types.str; 68 + default = ["--exclude-caches" "--exclude" "*.tmp"]; 69 + description = "Extra arguments passed to every `restic backup` invocation."; 70 + }; 71 + argsScheduled = lib.mkOption { 72 + type = lib.types.listOf lib.types.str; 73 + default = ["--tag" "scheduled"]; 74 + description = "Extra arguments passed only to scheduled `restic backup` runs."; 75 + }; 76 + }; 77 + prune = { 78 + args = lib.mkOption { 79 + type = lib.types.listOf lib.types.str; 80 + default = [ 81 + "--keep-last" 82 + "4" 83 + "--keep-hourly" 84 + "24" 85 + "--keep-daily" 86 + "7" 87 + "--keep-weekly" 88 + "4" 89 + "--keep-monthly" 90 + "12" 91 + ]; 92 + description = "Arguments passed to `restic forget --prune`."; 93 + }; 94 + }; 95 + check = { 96 + args = lib.mkOption { 97 + type = lib.types.listOf lib.types.str; 98 + default = []; 99 + description = "Arguments passed to `restic check`."; 100 + }; 101 + }; 102 + mount = { 103 + args = lib.mkOption { 104 + type = lib.types.listOf lib.types.str; 105 + default = []; 106 + description = "Arguments passed to `restic mount`."; 107 + }; 108 + }; 109 + }; 110 + }; 111 + 112 + profileToToml = name: prof: 113 + lib.filterAttrsRecursive (_: v: v != null && v != [] && v != "") { 114 + inherit (prof) name; 115 + env_file = prof.envFile; 116 + retry_lock = prof.retryLock; 117 + pre_hook = prof.preHook; 118 + post_hook = prof.postHook; 119 + schedule = { 120 + cron = prof.schedule.cron; 121 + on_battery = prof.schedule.onBattery; 122 + backup = prof.schedule.backup; 123 + prune = prof.schedule.prune; 124 + check = prof.schedule.check; 125 + }; 126 + backup = { 127 + paths = prof.backup.paths; 128 + args = prof.backup.args; 129 + args_scheduled = prof.backup.argsScheduled; 130 + }; 131 + prune.args = prof.prune.args; 132 + check.args = prof.check.args; 133 + mount.args = prof.mount.args; 134 + }; 135 + 136 + toml = pkgs.formats.toml {}; 137 + 138 + configFile = profiles: 139 + toml.generate "restray-config.toml" { 140 + profiles = lib.mapAttrsToList profileToToml profiles; 141 + }; 142 + in { 143 + inherit profileOpts configFile; 144 + }