feat: lazysecrets list prints everything lazysecrets can find, for fzf and editor pickers #70

Merged
keb merged 45 commits from feat/list into main 2026-10-14 21:30:44 +02:00
4 changed files with 65 additions and 19 deletions
Showing only changes of commit a4002a785c - Show all commits

No files matched your search

+22 -10
View File
@@ -90,19 +90,29 @@ func cmpOr(a, b string) string {
} }
// refArg parses get's and open's arguments: a reference, and optionally // refArg parses get's and open's arguments: a reference, and optionally
// the account to use when several could (lazysecrets list's column). // the account to use when several could (lazysecrets list's column) and,
func refArg(cmd string, args []string) (ref.Ref, error) { // for open, what to do with it rather than asking (--action).
func refArg(cmd string, args []string) (r ref.Ref, action string, err error) {
fs := flag.NewFlagSet(cmd, flag.ContinueOnError) fs := flag.NewFlagSet(cmd, flag.ContinueOnError)
account := fs.String("account", "", "the account to use when several could, as lazysecrets list prints it") account := fs.String("account", "", "the account to use when several could, as lazysecrets list prints it")
usage := "usage: lazysecrets get [--account <account>] <ref>"
act := new(string)
if cmd == "open" {
act = fs.String("action", "", "do this rather than ask: go (to it), copy (go, and copy its value) or show (go, and show it)")
usage = "usage: lazysecrets open [--account <account>] [--action go|copy|show] <ref>"
}
if err := fs.Parse(args); err != nil { if err := fs.Parse(args); err != nil {
return ref.Ref{}, err return r, "", err
} }
if fs.NArg() != 1 { if fs.NArg() != 1 {
return ref.Ref{}, fmt.Errorf("usage: lazysecrets %s [--account <account>] <ref>", cmd) return r, "", errors.New(usage)
} }
r, err := ref.Parse(fs.Arg(0)) if _, ok := app.OpenActions[*act]; *act != "" && !ok {
return r, "", fmt.Errorf("--action: %q is not go, copy or show", *act)
}
r, err = ref.Parse(fs.Arg(0))
r.Account = *account r.Account = *account
return r, err return r, *act, err
} }
// defaultConfigPath is $LAZYSECRETS_CONFIG_FILE, else config.yaml in the // defaultConfigPath is $LAZYSECRETS_CONFIG_FILE, else config.yaml in the
@@ -131,7 +141,7 @@ func run() error {
flag.Usage = func() { flag.Usage = func() {
fmt.Fprintf(flag.CommandLine.Output(), `Usage: fmt.Fprintf(flag.CommandLine.Output(), `Usage:
lazysecrets [flags] browse secrets lazysecrets [flags] browse secrets
lazysecrets [flags] [open [--account <account>]] <ref> lazysecrets [flags] [open [--account <account>] [--action go|copy|show]] <ref>
open a lazysecrets:// link (also a share link), open a lazysecrets:// link (also a share link),
Key Vault URL, @Microsoft.KeyVault(...) or Key Vault URL, @Microsoft.KeyVault(...) or
{{secrets/scope/key}} {{secrets/scope/key}}
@@ -168,6 +178,7 @@ Flags:
} }
var open, getRef *ref.Ref var open, getRef *ref.Ref
var openAction string
var shareArgs, listArgs []string var shareArgs, listArgs []string
printKeys := false printKeys := false
switch args := flag.Args(); { switch args := flag.Args(); {
@@ -181,7 +192,7 @@ Flags:
case len(args) == 1 && args[0] == "keybindings": case len(args) == 1 && args[0] == "keybindings":
printKeys = true printKeys = true
case len(args) > 0 && args[0] == "get": case len(args) > 0 && args[0] == "get":
r, err := refArg("get", args[1:]) r, _, err := refArg("get", args[1:])
if err != nil { if err != nil {
return err return err
} }
@@ -194,11 +205,11 @@ Flags:
return updateCmd(*configPath, args[1:]) return updateCmd(*configPath, args[1:])
case len(args) == 0: case len(args) == 0:
case args[0] == "open" && len(args) >= 2: case args[0] == "open" && len(args) >= 2:
r, err := refArg("open", args[1:]) r, act, err := refArg("open", args[1:])
if err != nil { if err != nil {
return err return err
} }
open = &r open, openAction = &r, act
case len(args) == 1 && looksLikeRef(args[0]): case len(args) == 1 && looksLikeRef(args[0]):
// lazysecrets <link>, like lazysecrets open <link> // lazysecrets <link>, like lazysecrets open <link>
r, err := ref.Parse(args[0]) r, err := ref.Parse(args[0])
@@ -464,6 +475,7 @@ Flags:
ShareKey: cfg.Sharing.Key, ShareKey: cfg.Sharing.Key,
ShareRules: shareRules(cfg), ShareRules: shareRules(cfg),
Open: open, Open: open,
OpenAction: openAction,
Styles: &theme, Styles: &theme,
DefaultFg: defaultFg, DefaultFg: defaultFg,
}) })
+7 -2
View File
@@ -121,6 +121,9 @@ account) and jumps to it, asking which account to use when several can see
it, ending on a secret's details as `enter` on it would. Links say what, it, ending on a secret's details as `enter` on it would. Links say what,
not what to do: opening a link to a secret asks whether to just go to it, not what to do: opening a link to a secret asks whether to just go to it,
also copy or show its value, or (`y`) copy its value without going there. also copy or show its value, or (`y`) copy its value without going there.
`lazysecrets open --action go|copy|show <link>` does one without asking,
and `--account <account>` picks the account (as [`list`](#listing-for-fzf-and-pickers)
prints it) when several can see it.
## In scripts ## In scripts
@@ -153,7 +156,7 @@ instead, showing the names and acting on the links:
```sh ```sh
lazysecrets list | fzf --ansi --with-nth='2..-3' --accept-nth=-1 \ lazysecrets list | fzf --ansi --with-nth='2..-3' --accept-nth=-1 \
--bind 'ctrl-o:become(lazysecrets open --account {-2} {-1})' \ --bind 'ctrl-o:become(lazysecrets open --action go --account {-2} {-1})' \
--bind 'ctrl-y:execute-silent(lazysecrets get --account {-2} {-1} | pbcopy)' --bind 'ctrl-y:execute-silent(lazysecrets get --account {-2} {-1} | pbcopy)'
``` ```
@@ -170,7 +173,9 @@ clipboard after `values.clipboardClear`, as one copied in lazysecrets is.
`--account` makes `open` and `get` use the account it was listed through, `--account` makes `open` and `get` use the account it was listed through,
when several of yours can see it: without, `open` asks which, and `get` when several of yours can see it: without, `open` asks which, and `get`
tries the one that last read it first. The link stays as it is, to copy tries the one that last read it first. The link stays as it is, to copy
and share; the account isn't part of it. and share; the account isn't part of it. `--action` tells `open` what to
do rather than asking, as it does for a link clicked elsewhere: `go` to
it, `copy` its value too, or `show` it.
What's [cached](config.md#cache) prints at once. Then, like Go to, it loads What's [cached](config.md#cache) prints at once. Then, like Go to, it loads
what's missing or older than `--max-age` (2m), and what's new in it follows what's missing or older than `--max-age` (2m), and what's new in it follows
+23 -7
View File
@@ -133,6 +133,9 @@ type Options struct {
NoHistory bool NoHistory bool
// Open is a reference to open at startup (lazysecrets open <ref>). // Open is a reference to open at startup (lazysecrets open <ref>).
Open *ref.Ref Open *ref.Ref
// OpenAction, if set, is what to do with Open, rather than asking:
// go to it, and maybe copy or show its value (OpenActions).
OpenAction string
// Styles, if set, replaces the default theme (styles.FromTheme), and // Styles, if set, replaces the default theme (styles.FromTheme), and
// DefaultFg the terminal's default text colour. // DefaultFg the terminal's default text colour.
Styles *styles.Styles Styles *styles.Styles
@@ -283,6 +286,7 @@ type Model struct {
prompt *modal // popup that has the keyboard prompt *modal // popup that has the keyboard
linked []provider.Container // vaults opened from links, this session linked []provider.Container // vaults opened from links, this session
open *ref.Ref // reference to open at startup open *ref.Ref // reference to open at startup
openAction string // and what to do with it, or ask
shareKey string // the config's sharing.key shareKey string // the config's sharing.key
shareRules share.Rules // and sharing.keyRules shareRules share.Rules // and sharing.keyRules
@@ -447,7 +451,7 @@ func New(providers []provider.Provider, opts Options) Model {
} }
m.defaultFg = opts.DefaultFg m.defaultFg = opts.DefaultFg
m.help = newHelp(m.st) m.help = newHelp(m.st)
m.open = opts.Open m.open, m.openAction = opts.Open, opts.OpenAction
saved := m.store.Layout() saved := m.store.Layout()
m.split, m.heights = saved.Split, saved.Heights m.split, m.heights = saved.Split, saved.Heights
@@ -521,18 +525,26 @@ func (m Model) Init() tea.Cmd {
cmds = append(cmds, m.fetchLatest(false)) cmds = append(cmds, m.fetchLatest(false))
} }
if m.open != nil { if m.open != nil {
r := *m.open msg := openRefMsg{ref: *m.open, action: m.openAction}
cmds = append(cmds, func() tea.Msg { return openRefMsg{r} }) cmds = append(cmds, func() tea.Msg { return msg })
} }
return tea.Batch(cmds...) return tea.Batch(cmds...)
} }
// openRefMsg starts opening a reference from outside the key handler. // openRefMsg starts opening a reference from outside the key handler:
type openRefMsg struct{ ref ref.Ref } // asking what to do with it, or doing action (OpenActions).
type openRefMsg struct {
ref ref.Ref
action string
}
// OpenActions are what lazysecrets open --action takes, the Open link
// menu's choices that go there.
var OpenActions = map[string]linkAction{"go": actGo, "copy": actCopy, "show": actShow}
// OpenLink is a message offering r in the Open link popup, for links // OpenLink is a message offering r in the Open link popup, for links
// handed over by the link handler (Program.Send). // handed over by the link handler (Program.Send).
func OpenLink(r ref.Ref) tea.Msg { return openRefMsg{r} } func OpenLink(r ref.Ref) tea.Msg { return openRefMsg{ref: r} }
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmds []tea.Cmd var cmds []tea.Cmd
@@ -589,7 +601,11 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case openRefMsg: case openRefMsg:
m.deferHandlerSetup() // the link first m.deferHandlerSetup() // the link first
cmds = append(cmds, m.offerRef("Open link", msg.ref, false)) if act, ok := OpenActions[msg.action]; ok && !msg.ref.IsShare() {
cmds = append(cmds, m.openRef(msg.ref, act))
} else {
cmds = append(cmds, m.offerRef("Open link", msg.ref, false))
}
case resolvedMsg: case resolvedMsg:
m.handleResolved(msg) m.handleResolved(msg)
+13
View File
@@ -206,6 +206,19 @@ func TestOpenPromptAndStartupOpen(t *testing.T) {
if _, _, s := d2.selected(); s != "api-key" { if _, _, s := d2.selected(); s != "api-key" {
t.Errorf("open at startup: selected %q", s) t.Errorf("open at startup: selected %q", s)
} }
// --action: no question, it does that.
d3 := newDriverOpts(t, Options{Open: &r, OpenAction: "go"})
if d3.m.prompt != nil {
t.Fatalf("--action go asked: %q", d3.m.prompt.title)
}
if _, _, s := d3.selected(); s != "api-key" {
t.Errorf("--action go: selected %q", s)
}
d4 := newDriverOpts(t, Options{Open: &r, OpenAction: "copy"})
if _, _, s := d4.selected(); s != "api-key" || d4.clip().content == "" {
t.Errorf("--action copy: selected %q, copied %q", s, d4.clip().content)
}
} }
func TestShareLinks(t *testing.T) { func TestShareLinks(t *testing.T) {