From 386af6656578859db8613ae964a0fc8ec60b8030 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karl-Einar=20Bergstr=C3=B8m?= Date: Thu, 15 Oct 2026 08:51:56 +0200 Subject: [PATCH] refactor: tidy the command's setup, the update command and instance reach - main: the subcommand is a name rather than nil-or-empty argument slices; the cache file and start directory are worked out once; config values with a default go through one valueOr helper. - The link handler's --config argument comes from one configArgs helper. - lazysecrets update parses its flags like the other subcommands, so -check and --check=true work too, and tells a development build by its dev version. - Instance reach is named (unreachable, switchable, inView), tmux client activity is compared as a number, and the best client loop starts from the first. - update: serverTimeout (it bounds the checksums request too), and parentheses in the platform check. --- cmd/lazysecrets/handler.go | 18 +++--- cmd/lazysecrets/main.go | 111 ++++++++++++++-------------------- cmd/lazysecrets/update.go | 13 ++-- cmd/lazysecrets/version.go | 3 +- internal/instance/instance.go | 13 ++-- internal/instance/tmux.go | 49 ++++++++------- internal/update/update.go | 12 ++-- 7 files changed, 105 insertions(+), 114 deletions(-) diff --git a/cmd/lazysecrets/handler.go b/cmd/lazysecrets/handler.go index 9e480c7..30043ac 100644 --- a/cmd/lazysecrets/handler.go +++ b/cmd/lazysecrets/handler.go @@ -69,11 +69,7 @@ func installHandler(cfg config.Config, configPath, term string) (string, string, if _, err := launchCommand(cfg, configPath, term, exe, "lazysecrets://check"); err != nil { return "", "", err } - launch := []string{exe} - if configPath != defaultConfigPath() { - launch = append(launch, "--config", configPath) - } - launch = append(launch, "launch") + launch := append(append([]string{exe}, configArgs(configPath)...), "launch") if term != "" { launch = append(launch, "--terminal", term) } @@ -188,8 +184,14 @@ func launchCommand(cfg config.Config, configPath, term, exe, link string) ([]str // innerCommand runs lazysecrets on link, with the environment it needs. func innerCommand(configPath, exe, link string) []string { inner := append(append([]string{"/usr/bin/env"}, linkhandler.Env(os.Getenv)...), exe) - if configPath != defaultConfigPath() { - inner = append(inner, "--config", configPath) - } + inner = append(inner, configArgs(configPath)...) return append(inner, "open", link) } + +// configArgs passes the config files on, unless they're the default. +func configArgs(configPath string) []string { + if configPath == defaultConfigPath() { + return nil + } + return []string{"--config", configPath} +} diff --git a/cmd/lazysecrets/main.go b/cmd/lazysecrets/main.go index 736fb1f..6ae53b4 100644 --- a/cmd/lazysecrets/main.go +++ b/cmd/lazysecrets/main.go @@ -177,26 +177,22 @@ Flags: return nil } - var open, getRef *ref.Ref + sub := "" // the subcommand; "" browses + var subArgs []string + var open *ref.Ref + var getRef ref.Ref var openAction string - var shareArgs, listArgs []string - printKeys := false switch args := flag.Args(); { - case len(args) > 0 && args[0] == "share": - shareArgs = args[1:] - if len(shareArgs) == 0 { - shareArgs = []string{} // still sharing: says how - } - case len(args) > 0 && args[0] == "list": - listArgs = append([]string{}, args[1:]...) // not nil: listing + case len(args) > 0 && (args[0] == "share" || args[0] == "list"): + sub, subArgs = args[0], args[1:] case len(args) == 1 && args[0] == "keybindings": - printKeys = true + sub = "keybindings" case len(args) > 0 && args[0] == "get": r, _, err := refArg("get", args[1:]) if err != nil { return err } - getRef = &r + sub, getRef = "get", r case len(args) > 0 && args[0] == "handler": return handler(*configPath, args[1:]) case len(args) > 0 && args[0] == "launch": @@ -242,7 +238,7 @@ Flags: if err := checkCommandKeys(&keymap, cfg.CustomCommands); err != nil { return err } - if printKeys { + if sub == "keybindings" { fmt.Print(keymap.YAML()) return nil } @@ -270,25 +266,13 @@ Flags: } // timeoutFor is how long a request to a provider of kind k may take. timeoutFor := func(k provider.Kind) time.Duration { return cmp.Or(timeouts[k], *timeout) } - if !set["clipboard-clear"] && cfg.Values.ClipboardClear != nil { - *clipClear = *cfg.Values.ClipboardClear - } - expiryDays := 30 - if cfg.UI.ExpiryWarningDays != nil { - expiryDays = *cfg.UI.ExpiryWarningDays - } - revealTimeout := 60 * time.Second - if cfg.Values.RevealTimeout != nil { - revealTimeout = *cfg.Values.RevealTimeout - } - loadDelay := 150 * time.Millisecond - if cfg.UI.LoadDelay != nil { - loadDelay = *cfg.UI.LoadDelay - } - var updateEvery time.Duration // 0: the app's default, a day - if cfg.Updates.AutoCheckInterval != nil { - updateEvery = *cfg.Updates.AutoCheckInterval + if !set["clipboard-clear"] { + *clipClear = valueOr(cfg.Values.ClipboardClear, *clipClear) } + expiryWarning := time.Duration(valueOr(cfg.UI.ExpiryWarningDays, 30)) * 24 * time.Hour + revealTimeout := valueOr(cfg.Values.RevealTimeout, 60*time.Second) + loadDelay := valueOr(cfg.UI.LoadDelay, 150*time.Millisecond) + updateEvery := valueOr(cfg.Updates.AutoCheckInterval, 0) // 0: the app's default, a day if !set["icons"] { switch { case os.Getenv("LAZYSECRETS_ICONS") != "": @@ -298,7 +282,17 @@ Flags: } } - recentIcon := cfg.UI.RecentIcon != nil && *cfg.UI.RecentIcon + recentIcon := valueOr(cfg.UI.RecentIcon, false) + + startDir := *dir + if startDir == "" { + startDir, _ = os.Getwd() + } + // What was loaded last time. + cacheFile := "" + if d, err := config.CacheDir(); err == nil && !*demo && config.On(cfg.Storage.Cache) { + cacheFile = filepath.Join(d, "cache.json") + } // Stars, notes and the layout. Demo mode keeps them in memory, so // sample vaults never end up in the real file. @@ -356,17 +350,9 @@ Flags: providers = append(providers, dbx) } if config.On(pc.Repos.Enabled) { - start := *dir - if start == "" { - start, _ = os.Getwd() - } - roots := repo.DefaultRoots - if pc.Repos.Roots != nil { - roots = *pc.Repos.Roots - } repos := repo.New(repo.Settings{ - Dir: start, Paths: pc.Repos.Paths, Roots: roots, Starred: starredRepos(st), - Include: pc.Repos.Include, Exclude: pc.Repos.Exclude, + Dir: startDir, Paths: pc.Repos.Paths, Roots: valueOr(pc.Repos.Roots, repo.DefaultRoots), + Starred: starredRepos(st), Include: pc.Repos.Include, Exclude: pc.Repos.Exclude, }) // The repo it started in (shown on top as Repo) and the // others, checked for secret files in the background like @@ -382,19 +368,14 @@ Flags: // the standard logger, which would draw over the TUI. log.SetOutput(io.Discard) - if getRef != nil { + switch sub { + case "get": ctx, cancel := context.WithTimeout(context.Background(), timeoutFor(getRef.Kind)) defer cancel() - return get(ctx, providers, st, *getRef) - } - if listArgs != nil { - cachePath := "" - if d, err := config.CacheDir(); err == nil && !*demo && config.On(cfg.Storage.Cache) { - cachePath = filepath.Join(d, "cache.json") - } - return listCmd(providers, st, cachePath, timeoutFor, theme, *icons, recentIcon, time.Duration(expiryDays)*24*time.Hour, listArgs, os.Stdout) - } - if shareArgs != nil { + return get(ctx, providers, st, getRef) + case "list": + return listCmd(providers, st, cacheFile, timeoutFor, theme, *icons, recentIcon, expiryWarning, subArgs, os.Stdout) + case "share": // The files from a repo, the key from whichever provider it's // in: the longest timeout. d := *timeout @@ -403,7 +384,7 @@ Flags: } ctx, cancel := context.WithTimeout(context.Background(), d) defer cancel() - return shareFiles(ctx, providers, st, cfg.Sharing.Key, shareRules(cfg), shareArgs, os.Stdout) + return shareFiles(ctx, providers, st, cfg.Sharing.Key, shareRules(cfg), subArgs, os.Stdout) } // Links opened elsewhere (lazysecrets launch) can come here. @@ -437,16 +418,10 @@ Flags: onActive = srv.Touch } - // What was loaded last time, and where it was left in this folder. - var cacheFile, placeDir string + // Where it was left in this folder. + placeDir := "" if !*demo { - if d, err := config.CacheDir(); err == nil && config.On(cfg.Storage.Cache) { - cacheFile = filepath.Join(d, "cache.json") - } - placeDir = *dir - if placeDir == "" { - placeDir, _ = os.Getwd() - } + placeDir = startDir if abs, err := filepath.Abs(placeDir); err == nil { placeDir = abs } @@ -463,7 +438,7 @@ Flags: NoHistory: !config.On(cfg.Storage.History), OnActive: onActive, RevealTimeout: revealTimeout, - ExpiryWarning: time.Duration(expiryDays) * 24 * time.Hour, + ExpiryWarning: expiryWarning, HideOnBlur: config.On(cfg.Values.HideOnBlur), HandlerSetup: setup, Updater: updater(cfg.Updates.Sources), @@ -496,6 +471,14 @@ Flags: return err } +// valueOr is *p, or def when p is nil (not set in the config). +func valueOr[T any](p *T, def T) T { + if p == nil { + return def + } + return *p +} + // configFiles are the config files given, for the status pane: those // that exist, or the first if none does (where to create one). func configFiles(list string) []string { diff --git a/cmd/lazysecrets/update.go b/cmd/lazysecrets/update.go index 38d85c9..86c46ea 100644 --- a/cmd/lazysecrets/update.go +++ b/cmd/lazysecrets/update.go @@ -3,6 +3,7 @@ package main import ( "context" "errors" + "flag" "fmt" "strings" "time" @@ -32,8 +33,12 @@ func updater(servers []string) *app.Updater { // updateCmd is lazysecrets update [--check]: the newer release's // changelog, then (unless --check) install it. func updateCmd(configPath string, args []string) error { - check := len(args) == 1 && args[0] == "--check" - if len(args) > 0 && !check { + fs := flag.NewFlagSet("update", flag.ContinueOnError) + check := fs.Bool("check", false, "just say if there's a newer release") + if err := fs.Parse(args); err != nil { + return err + } + if fs.NArg() != 0 { return errors.New("usage: lazysecrets update [--check]") } cfg, err := config.LoadAll(strings.Split(configPath, ",")) @@ -49,14 +54,14 @@ func updateCmd(configPath string, args []string) error { return fmt.Errorf("looking for a newer lazysecrets: %w", err) } if !update.Newer(current, r.Tag) { - if !update.Newer(current, "v999.0.0") { + if strings.HasPrefix(current, "dev") { return fmt.Errorf("this is a development build (%s); the latest release is %s: install it with install.sh, or go install …@%s", current, r.Tag, r.Tag) } fmt.Printf("lazysecrets %s is the latest (%s)\n", current, r.Tag) return nil } fmt.Printf("lazysecrets %s is out (you have %s):\n\n%s\n\n", r.Tag, current, indent(r.Notes(current))) - if check { + if *check { fmt.Println("lazysecrets update installs it.") return nil } diff --git a/cmd/lazysecrets/version.go b/cmd/lazysecrets/version.go index 19ebf32..81b681e 100644 --- a/cmd/lazysecrets/version.go +++ b/cmd/lazysecrets/version.go @@ -3,7 +3,6 @@ package main import ( "regexp" "runtime/debug" - "strings" ) // version is set by release builds: @@ -31,7 +30,7 @@ func versionString() string { // go install …@. if m := pseudo.FindStringSubmatch(v); m != nil { dirty := "" - if strings.HasSuffix(v, "+dirty") { + if m[2] != "" { dirty = "-dirty" } return "dev-" + m[1][:7] + dirty diff --git a/internal/instance/instance.go b/internal/instance/instance.go index f15375f..9504c6b 100644 --- a/internal/instance/instance.go +++ b/internal/instance/instance.go @@ -36,15 +36,18 @@ func Dir(stateDir string) string { type Status struct { PID int `json:"pid"` LastActive time.Time `json:"lastActive"` // last key press or focus - // Reach is how readily it can be shown: 2 in view (outside tmux, or - // in a tmux session a client shows), 1 in a tmux session that a tmux - // client can be switched to, 0 not at all. - Reach int `json:"reach"` + Reach int `json:"reach"` // how readily it can be shown: unreachable, switchable or inView // App is the terminal app it would show in (macOS), preferring the // app the link was clicked in. App string `json:"app,omitempty"` } +const ( + unreachable = 0 + switchable = 1 // in a tmux session that a tmux client can be switched to + inView = 2 // outside tmux, or in a tmux session a client shows +) + type request struct { Op string `json:"op"` // "status" or "open" Link string `json:"link,omitempty"` @@ -188,7 +191,7 @@ func Best(found []Found, front string, terminal bool) (Found, bool) { best = f } } - return best, best.Reach > 0 + return best, best.Reach > unreachable } // Open hands link, clicked in front, to f. diff --git a/internal/instance/tmux.go b/internal/instance/tmux.go index c53e461..6603f7c 100644 --- a/internal/instance/tmux.go +++ b/internal/instance/tmux.go @@ -1,6 +1,7 @@ package instance import ( + "cmp" "os" "os/exec" "slices" @@ -25,9 +26,10 @@ func tmux(args ...string) (string, error) { func inTmux() bool { return os.Getenv("TMUX") != "" && os.Getenv("TMUX_PANE") != "" } type client struct { - name, session, activity string // activity: Unix seconds - pid int - app string + name, session string + activity int // Unix seconds + pid int + app string } // clients lists the tmux server's clients, the one last typed in first. @@ -42,30 +44,31 @@ func clients() []client { if len(f) < 4 { continue } - pid, err := strconv.Atoi(f[1]) - if err != nil { + activity, err1 := strconv.Atoi(f[0]) + pid, err2 := strconv.Atoi(f[1]) + if err1 != nil || err2 != nil { continue } - cs = append(cs, client{name: f[2], session: f[3], activity: f[0], pid: pid, app: linkhandler.HostApp(pid)}) + cs = append(cs, client{name: f[2], session: f[3], activity: activity, pid: pid, app: linkhandler.HostApp(pid)}) } - // Activity has the same width for centuries: strings compare like - // numbers. - slices.SortStableFunc(cs, func(a, b client) int { return strings.Compare(b.activity, a.activity) }) + slices.SortStableFunc(cs, func(a, b client) int { return cmp.Compare(b.activity, a.activity) }) return cs } // place is where this instance would show: the tmux client (zero outside -// tmux), the app, and reach: 2 already on screen there (outside tmux, or -// a client on its session), 1 after switching a client, 0 nowhere. +// tmux), the app, and its reach. func place(front string) (client, string, int) { if !inTmux() { - return client{}, linkhandler.HostApp(os.Getpid()), 2 + return client{}, linkhandler.HostApp(os.Getpid()), inView } session, err := tmux("display-message", "-p", "-t", os.Getenv("TMUX_PANE"), "#{session_name}") if err != nil { - return client{}, "", 0 + return client{}, "", unreachable } cs := clients() + if len(cs) == 0 { + return client{}, "", unreachable + } // Best first: in the front app on its session, in the front app, // on its session, any. rank := func(c client) int { @@ -78,20 +81,16 @@ func place(front string) (client, string, int) { } return r } - var best client - found := false - for _, c := range cs { - if !found || rank(c) > rank(best) { - best, found = c, true + best := cs[0] + for _, c := range cs[1:] { + if rank(c) > rank(best) { + best = c } } - if !found { - return client{}, "", 0 - } if best.session == session { - return best, best.app, 2 + return best, best.app, inView } - return best, best.app, 1 + return best, best.app, switchable } // Raise brings this instance into view for a link clicked in front: in @@ -99,14 +98,14 @@ func place(front string) (client, string, int) { // then its app to the front, showing a hidden dropdown terminal. func Raise(front string) { c, app, reach := place(front) - if reach == 0 { + if reach == unreachable { return } if inTmux() { pane := os.Getenv("TMUX_PANE") _, _ = tmux("select-window", "-t", pane) _, _ = tmux("select-pane", "-t", pane) - if reach == 1 { + if reach == switchable { _, _ = tmux("switch-client", "-c", c.name, "-t", pane) } } diff --git a/internal/update/update.go b/internal/update/update.go index c78de13..9edf301 100644 --- a/internal/update/update.go +++ b/internal/update/update.go @@ -45,9 +45,9 @@ const ( // DefaultServers are where releases are looked for, in order. var DefaultServers = []string{GitHubRepo, DefaultRepo} -// latestTimeout bounds asking one server for the latest release, so a -// server that's down leaves time for the next. -const latestTimeout = 5 * time.Second +// serverTimeout bounds asking one server for the latest release or its +// checksums, so a server that's down leaves time for the next. +const serverTimeout = 5 * time.Second // maxDownload bounds what's read from the server. const maxDownload = 200 << 20 @@ -153,7 +153,7 @@ func (c *Client) latest(ctx context.Context, repo string) (Release, error) { if err != nil { return Release{}, err } - ctx, cancel := context.WithTimeout(ctx, latestTimeout) + ctx, cancel := context.WithTimeout(ctx, serverTimeout) defer cancel() // The recent releases in one request (per_page on GitHub, limit on // Gitea), for their changelogs. @@ -222,7 +222,7 @@ func (c *Client) Install(ctx context.Context, r Release, exe string) error { if m := Managed(exe); m != "" { return fmt.Errorf("%s is managed by %s: update it there", exe, m) } - if runtime.GOOS != "darwin" && runtime.GOOS != "linux" || runtime.GOARCH != "amd64" && runtime.GOARCH != "arm64" { + if (runtime.GOOS != "darwin" && runtime.GOOS != "linux") || (runtime.GOARCH != "amd64" && runtime.GOARCH != "arm64") { return fmt.Errorf("no release build for %s/%s", runtime.GOOS, runtime.GOARCH) } version := strings.TrimPrefix(r.Tag, "v") @@ -272,7 +272,7 @@ func (c *Client) download(ctx context.Context, tag, file string) ([]byte, error) var errs []error for _, repo := range c.servers() { base := strings.TrimSuffix(repo, "/") + "/releases/download/" + tag + "/" - sctx, cancel := context.WithTimeout(ctx, latestTimeout) + sctx, cancel := context.WithTimeout(ctx, serverTimeout) sums, err := c.get(sctx, base+"checksums.txt", 1<<20) cancel() if err != nil {