diff --git a/cmd/lazysecrets/main.go b/cmd/lazysecrets/main.go index a0f214b..223cd85 100644 --- a/cmd/lazysecrets/main.go +++ b/cmd/lazysecrets/main.go @@ -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 ] " + 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 ] [--action go|copy|show] " + } 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 ] ", 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 ]] + lazysecrets [flags] [open [--account ] [--action go|copy|show]] 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 , like lazysecrets open 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, }) diff --git a/docs/links.md b/docs/links.md index 01dd0fc..562ae36 100644 --- a/docs/links.md +++ b/docs/links.md @@ -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 ` does one without asking, +and `--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 diff --git a/internal/ui/app/app.go b/internal/ui/app/app.go index 37fc60e..048ba89 100644 --- a/internal/ui/app/app.go +++ b/internal/ui/app/app.go @@ -133,6 +133,9 @@ type Options struct { NoHistory bool // Open is a reference to open at startup (lazysecrets open ). 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) diff --git a/internal/ui/app/links_test.go b/internal/ui/app/links_test.go index d57d0d7..a5f5071 100644 --- a/internal/ui/app/links_test.go +++ b/internal/ui/app/links_test.go @@ -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) {