diff --git a/README.md b/README.md index bdfc920..e321629 100644 --- a/README.md +++ b/README.md @@ -102,7 +102,7 @@ names and metadata of what you've loaded in a cache file readable only by you, to show it at once next time (`cache: false` turns it off). It talks only to the services you use it with, and collects no usage data. The one call of its own is a daily check for a new release, on GitHub or else -git.kebcloud.no, which `updateCheck: false` turns off. +git.kebcloud.no, which `updates.autoCheck: false` turns off. ## Repositories diff --git a/cmd/lazysecrets/main.go b/cmd/lazysecrets/main.go index 23a50db..496356c 100644 --- a/cmd/lazysecrets/main.go +++ b/cmd/lazysecrets/main.go @@ -225,6 +225,10 @@ Flags: if cfg.LoadDelay != nil { loadDelay = *cfg.LoadDelay } + var updateEvery time.Duration // 0: the app's default, a day + if cfg.Updates.AutoCheckInterval != nil { + updateEvery = *cfg.Updates.AutoCheckInterval + } if !set["icons"] { switch { case os.Getenv("LAZYSECRETS_ICONS") != "": @@ -379,8 +383,9 @@ Flags: ExpiryWarning: time.Duration(expiryDays) * 24 * time.Hour, HideOnBlur: config.On(cfg.HideOnBlur), HandlerSetup: setup, - Updater: updater(cfg.Releases), - UpdateCheck: config.On(cfg.UpdateCheck), + Updater: updater(cfg.Updates.UpdateSources), + UpdateCheck: config.On(cfg.Updates.AutoCheck), + UpdateEvery: updateEvery, Keys: &keymap, Commands: cfg.CustomCommands, Version: versionString(), diff --git a/cmd/lazysecrets/update.go b/cmd/lazysecrets/update.go index a56111b..388c29f 100644 --- a/cmd/lazysecrets/update.go +++ b/cmd/lazysecrets/update.go @@ -43,7 +43,7 @@ func updateCmd(configPath string, args []string) error { current := versionString() ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) defer cancel() - c := &update.Client{Servers: cfg.Releases} + c := &update.Client{Servers: cfg.Updates.UpdateSources} r, err := c.Latest(ctx) if err != nil { return fmt.Errorf("looking for a newer lazysecrets: %w", err) diff --git a/docs/config.md b/docs/config.md index e38a3ee..e2c0686 100644 --- a/docs/config.md +++ b/docs/config.md @@ -16,10 +16,12 @@ timeout: 30s # per backend request loadDelay: 150ms # how long the cursor rests on a row before it loads; 0s loads at once sourcesView: grouped # or flat; ` toggles it collapseGroups: true # start with the Sources pane's groups folded; enter or space unfolds one -updateCheck: true # look for a newer release once a day (U and lazysecrets update work either way) -releases: # where updates come from, tried in order (GitHub, or a Gitea or Forgejo server) - - https://github.com/karleinarb/lazysecrets - - https://git.kebcloud.no/keb/lazysecrets +updates: + autoCheck: true # look for a newer release at startup (U and lazysecrets update work either way) + autoCheckInterval: 24h # at most this often + updateSources: # where updates come from, tried in order (GitHub, or a Gitea or Forgejo server) + - https://github.com/karleinarb/lazysecrets + - https://git.kebcloud.no/keb/lazysecrets history: true # count uses of items for Recent (R) cache: true # keep what was loaded (never values) for the next start providers: @@ -136,11 +138,12 @@ are read from disk anyway. Deleting the file is always safe, and ## Updates -Once a day (`updateCheck`), in the background, lazysecrets asks for the -latest release; within the day it reuses the answer from the state file. -Releases are published on GitHub and on git.kebcloud.no, and it asks -them in that order, the next when one doesn't answer within 5 seconds; -`releases` lists others, such as a mirror of your own. Offline, or with +At startup, in the background, lazysecrets asks for the latest release +(`updates.autoCheck`), once a day or every `updates.autoCheckInterval`; +until then it reuses the answer from the state file. Releases are +published on GitHub and on git.kebcloud.no, and it asks them in that +order, the next when one doesn't answer within 5 seconds; +`updates.updateSources` lists others, such as a mirror of your own. Offline, or with every server down, it says nothing. A newer release shows in the footer, `v0.1.1 ↑ v0.2.0`; `U` (or `lazysecrets update --check`) shows its changelog. diff --git a/internal/config/config.go b/internal/config/config.go index e8a59df..6ec30cb 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -38,14 +38,8 @@ type Config struct { SourcesView string `yaml:"sourcesView"` // History counts uses of items for the Recent list; defaults to true. History *bool `yaml:"history"` - // UpdateCheck looks for a newer release once a day, in the - // background; defaults to true. U and lazysecrets update work either - // way. - UpdateCheck *bool `yaml:"updateCheck"` - // Releases are the repositories updates come from, tried in order: - // on GitHub, or a Gitea or Forgejo server. Defaults to GitHub, then - // git.kebcloud.no (update.DefaultServers). - Releases []string `yaml:"releases"` + // Updates sets how lazysecrets looks for newer releases. + Updates Updates `yaml:"updates"` // Cache keeps what was loaded (never values) between runs, so lists // show at once and refresh in the background; defaults to true. Cache *bool `yaml:"cache"` @@ -68,6 +62,19 @@ type Config struct { Theme map[string][]string `yaml:"theme"` } +type Updates struct { + // AutoCheck looks for a newer release in the background at startup, + // at most once per AutoCheckInterval; defaults to true. U and + // lazysecrets update work either way. + AutoCheck *bool `yaml:"autoCheck"` + // AutoCheckInterval defaults to 24h. + AutoCheckInterval *time.Duration `yaml:"autoCheckInterval"` + // UpdateSources are the repositories updates come from, tried in + // order: on GitHub, or a Gitea or Forgejo server. Defaults to GitHub, + // then git.kebcloud.no (update.DefaultServers). + UpdateSources []string `yaml:"updateSources"` +} + type LinkHandler struct { // Terminal to open links in, overriding the one handler install // detected: kitty, ghostty, wezterm, iterm2, terminal, alacritty, @@ -236,9 +243,12 @@ func (c Config) validate(path string) error { if d := c.LoadDelay; d != nil && (*d < 0 || *d > 5*time.Second) { return fmt.Errorf("%s: loadDelay: %s is not between 0s and 5s", path, *d) } - for i, r := range c.Releases { + if d := c.Updates.AutoCheckInterval; d != nil && *d <= 0 { + return fmt.Errorf("%s: updates.autoCheckInterval: %s is not positive (autoCheck: false turns it off)", path, *d) + } + for i, r := range c.Updates.UpdateSources { if _, err := update.API(r); err != nil || !strings.HasPrefix(r, "https://") { - return fmt.Errorf("%s: releases[%d]: %q is not an https:// repository address, like %s", path, i, r, update.GitHubRepo) + return fmt.Errorf("%s: updates.updateSources[%d]: %q is not an https:// repository address, like %s", path, i, r, update.GitHubRepo) } } if k := c.Share.Key; k != "" { diff --git a/internal/config/config_test.go b/internal/config/config_test.go index e5d2e94..06736d2 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -70,8 +70,9 @@ func TestLoadRejectsMistakes(t *testing.T) { "old layout": "azure:\n vaults: [https://kv.vault.azure.net/]\n", "duration": "timeout: soon\n", "view": "sourcesView: tree\n", - "releases": "releases: [git.example.com/team/lazysecrets]\n", - "http": "releases: [http://git.example.com/team/lazysecrets]\n", + "sources": "updates:\n updateSources: [git.example.com/team/lazysecrets]\n", + "http": "updates:\n updateSources: [http://git.example.com/team/lazysecrets]\n", + "interval": "updates:\n autoCheckInterval: 0s\n", "share key": "share:\n key: https://kv.vault.azure.net/\n", "workspace": "share:\n key: \"{{secrets/team/env-share}}\"\n", "short keys": "share:\n keyRules:\n minLength: 8\n", @@ -119,10 +120,10 @@ theme: } } -func TestReleases(t *testing.T) { - c, err := Load(write(t, "releases: [https://git.example.com/team/lazysecrets]\n")) - if err != nil || len(c.Releases) != 1 { - t.Errorf("releases = %v, %v", c.Releases, err) +func TestUpdates(t *testing.T) { + c, err := Load(write(t, "updates:\n autoCheck: false\n autoCheckInterval: 168h\n updateSources: [https://git.example.com/team/lazysecrets]\n")) + if err != nil || On(c.Updates.AutoCheck) || *c.Updates.AutoCheckInterval != 7*24*time.Hour || len(c.Updates.UpdateSources) != 1 { + t.Errorf("updates = %+v, %v", c.Updates, err) } } diff --git a/internal/ui/app/app.go b/internal/ui/app/app.go index e023152..45ab4ee 100644 --- a/internal/ui/app/app.go +++ b/internal/ui/app/app.go @@ -117,9 +117,11 @@ type Options struct { // Keys, if set, replaces the default keymap (keys.Map.Apply). Keys *keys.Map // Updater, if set, finds and installs newer releases (U); with - // UpdateCheck, lazysecrets also looks once a day on its own. + // UpdateCheck, lazysecrets also looks on its own at startup, at most + // once per UpdateEvery (0 for a day). Updater *Updater UpdateCheck bool + UpdateEvery time.Duration // HandlerSetup, if set, offers to set up the link handler at startup. HandlerSetup *HandlerSetup // NoHistory stops counting uses of items for the Recent list. @@ -236,6 +238,7 @@ type Model struct { updater *Updater updateCheck bool + updateEvery time.Duration fetchOnStart bool // look for a newer release in Init newer *update.Release // a newer release to offer updated string // the release installed, waiting for a restart @@ -427,7 +430,10 @@ func New(providers []provider.Provider, opts Options) Model { m.revealTimeout, m.hideOnBlur = opts.RevealTimeout, opts.HideOnBlur m.expiryWarning = opts.ExpiryWarning m.handlerSetup = opts.HandlerSetup - m.updater, m.updateCheck = opts.Updater, opts.UpdateCheck + m.updater, m.updateCheck, m.updateEvery = opts.Updater, opts.UpdateCheck, opts.UpdateEvery + if m.updateEvery == 0 { + m.updateEvery = updateEvery + } m.startUpdateCheck() m.askHandler = opts.HandlerSetup != nil && !m.store.Flag(flagNoHandlerPrompt) m.settle = opts.LoadDelay diff --git a/internal/ui/app/update.go b/internal/ui/app/update.go index 37b019c..006b2ff 100644 --- a/internal/ui/app/update.go +++ b/internal/ui/app/update.go @@ -20,7 +20,8 @@ type Updater struct { } // updateEvery is how often lazysecrets looks for a newer release on its -// own; within that, the last answer (in the state file) is used. +// own unless the config says otherwise; within that, the last answer (in +// the state file) is used. const updateEvery = 24 * time.Hour type updateMsg struct { @@ -35,14 +36,14 @@ type installedMsg struct { } // startUpdateCheck sets up the startup look for a newer release: the -// state file's answer when it looked less than a day ago, else a quiet +// state file's answer when it looked within updateEvery, else a quiet // look at the server (fetchOnStart, for Init). func (m *Model) startUpdateCheck() { if m.updater == nil || !m.updateCheck { return } last := m.store.UpdateCheck() - if m.now().Sub(last.At) < updateEvery { + if m.now().Sub(last.At) < m.updateEvery { m.offer(update.Release{Tag: last.Tag, Body: last.Notes, URL: last.URL}) return } diff --git a/internal/ui/app/update_test.go b/internal/ui/app/update_test.go index 08b7c55..8bda118 100644 --- a/internal/ui/app/update_test.go +++ b/internal/ui/app/update_test.go @@ -83,6 +83,12 @@ func TestUpdate(t *testing.T) { if f2.asked != 1 { t.Errorf("asked %d times a day later", f2.asked) } + // With autoCheckInterval: 1h, two hours on is long enough. + mem.SetUpdateCheck(store.UpdateCheck{At: time.Now().Add(-2 * time.Hour), Tag: "v0.2.0"}) + newDriverOpts(t, Options{Version: "0.1.1", Updater: f2.updater(), UpdateCheck: true, UpdateEvery: time.Hour, Store: mem}) + if f2.asked != 2 { + t.Errorf("asked %d times, want a look after the hour", f2.asked) + } } func TestUpdateAsked(t *testing.T) {