refactor: tidy the command's setup, the update command and instance reach #73

Merged
keb merged 1 commits from refactor/cmd-cleanups into main 2026-10-15 10:40:41 +02:00
7 changed files with 103 additions and 112 deletions
Showing only changes of commit f9a7ed73be - Show all commits

No files matched your search

+10 -8
View File
@@ -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}
}
+47 -64
View File
@@ -171,26 +171,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":
@@ -236,7 +232,7 @@ Flags:
if err := checkCommandKeys(&keymap, cfg.CustomCommands); err != nil {
return err
}
if printKeys {
if sub == "keybindings" {
fmt.Print(keymap.YAML())
return nil
}
@@ -264,25 +260,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") != "":
@@ -292,7 +276,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.
@@ -350,17 +344,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
@@ -376,19 +362,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
@@ -397,7 +378,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.
@@ -431,16 +412,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
}
@@ -457,7 +432,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),
@@ -490,6 +465,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 {
+9 -4
View File
@@ -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
}
+1 -2
View File
@@ -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 …@<commit>.
if m := pseudo.FindStringSubmatch(v); m != nil {
dirty := ""
if strings.HasSuffix(v, "+dirty") {
if m[2] != "" {
dirty = "-dirty"
}
return "dev-" + m[1][:7] + dirty
+8 -5
View File
@@ -35,15 +35,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"`
@@ -187,7 +190,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.
+22 -23
View File
@@ -1,6 +1,7 @@
package instance
import (
"cmp"
"os"
"os/exec"
"slices"
@@ -25,7 +26,8 @@ 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
name, session string
activity int // Unix seconds
pid int
app string
}
@@ -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)
}
}
+6 -6
View File
@@ -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
@@ -151,7 +151,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.
@@ -220,7 +220,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")
@@ -270,7 +270,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 {