feat: lazysecrets open --action goes, copies or shows without asking

This commit is contained in:
keb committed 2026-10-14 21:24:01 +02:00
1 parent b05d19209c
commit a4002a785c
4 files changed
+65 -19

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
// the account to use when several could (lazysecrets list's column).
func refArg(cmd string, args []string) (ref.Ref, error) {
// the account to use when several could (lazysecrets list's column) and,
// 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)
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 {
return ref.Ref{}, err
return r, "", err
}
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
return r, err
return r, *act, err
}
// defaultConfigPath is $LAZYSECRETS_CONFIG_FILE, else config.yaml in the
@@ -131,7 +141,7 @@ func run() error {
flag.Usage = func() {
fmt.Fprintf(flag.CommandLine.Output(), `Usage:
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),
Key Vault URL, @Microsoft.KeyVault(...) or
{{secrets/scope/key}}
@@ -168,6 +178,7 @@ Flags:
}
var open, getRef *ref.Ref
var openAction string
var shareArgs, listArgs []string
printKeys := false
switch args := flag.Args(); {
@@ -181,7 +192,7 @@ Flags:
case len(args) == 1 && args[0] == "keybindings":
printKeys = true
case len(args) > 0 && args[0] == "get":
r, err := refArg("get", args[1:])
r, _, err := refArg("get", args[1:])
if err != nil {
return err
}
@@ -194,11 +205,11 @@ Flags:
return updateCmd(*configPath, args[1:])
case len(args) == 0:
case args[0] == "open" && len(args) >= 2:
r, err := refArg("open", args[1:])
r, act, err := refArg("open", args[1:])
if err != nil {
return err
}
open = &r
open, openAction = &r, act
case len(args) == 1 && looksLikeRef(args[0]):
// lazysecrets <link>, like lazysecrets open <link>
r, err := ref.Parse(args[0])
@@ -464,6 +475,7 @@ Flags:
ShareKey: cfg.Sharing.Key,
ShareRules: shareRules(cfg),
Open: open,
OpenAction: openAction,
Styles: &theme,
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,
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.
`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
@@ -153,7 +156,7 @@ instead, showing the names and acting on the links:
```sh
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)'
```
@@ -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,
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
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 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
// Open is a reference to open at startup (lazysecrets open <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
// DefaultFg the terminal's default text colour.
Styles *styles.Styles
@@ -283,6 +286,7 @@ type Model struct {
prompt *modal // popup that has the keyboard
linked []provider.Container // vaults opened from links, this session
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
shareRules share.Rules // and sharing.keyRules
@@ -447,7 +451,7 @@ func New(providers []provider.Provider, opts Options) Model {
}
m.defaultFg = opts.DefaultFg
m.help = newHelp(m.st)
m.open = opts.Open
m.open, m.openAction = opts.Open, opts.OpenAction
saved := m.store.Layout()
m.split, m.heights = saved.Split, saved.Heights
@@ -521,18 +525,26 @@ func (m Model) Init() tea.Cmd {
cmds = append(cmds, m.fetchLatest(false))
}
if m.open != nil {
r := *m.open
cmds = append(cmds, func() tea.Msg { return openRefMsg{r} })
msg := openRefMsg{ref: *m.open, action: m.openAction}
cmds = append(cmds, func() tea.Msg { return msg })
}
return tea.Batch(cmds...)
}
// openRefMsg starts opening a reference from outside the key handler.
type openRefMsg struct{ ref ref.Ref }
// openRefMsg starts opening a reference from outside the key handler:
// 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
// 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) {
var cmds []tea.Cmd
@@ -589,7 +601,11 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case openRefMsg:
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:
m.handleResolved(msg)
+13
View File
@@ -206,6 +206,19 @@ func TestOpenPromptAndStartupOpen(t *testing.T) {
if _, _, s := d2.selected(); s != "api-key" {
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) {