feat: lazysecrets list prints everything lazysecrets can find, for fzf and editor pickers #70
No files matched your search
+48
-7
@@ -13,38 +13,50 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"charm.land/lipgloss/v2"
|
||||
|
||||
"git.kebcloud.no/keb/lazysecrets/internal/cachefile"
|
||||
"git.kebcloud.no/keb/lazysecrets/internal/listing"
|
||||
"git.kebcloud.no/keb/lazysecrets/internal/provider"
|
||||
"git.kebcloud.no/keb/lazysecrets/internal/ui/icons"
|
||||
"git.kebcloud.no/keb/lazysecrets/internal/ui/styles"
|
||||
)
|
||||
|
||||
// listCmd prints every source, vault and secret lazysecrets can find, one
|
||||
// per line, for fzf, editor pickers and scripts:
|
||||
//
|
||||
// lazysecrets list | fzf --delimiter='\t' --with-nth=3 | cut -f1
|
||||
// lazysecrets list | fzf --ansi --delimiter='\t' --with-nth=3 | cut -f1
|
||||
//
|
||||
// The name is coloured from the theme, with the provider's icon (as in
|
||||
// the TUI: --icons, LAZYSECRETS_ICONS, ui.icons); the link never is.
|
||||
//
|
||||
// What's cached prints at once; what's missing or older than --max-age
|
||||
// follows as it loads. Lists that can't be read are reported on stderr.
|
||||
func listCmd(providers []provider.Provider, cachePath string, timeout func(provider.Kind) time.Duration, args []string, out, errOut io.Writer) error {
|
||||
func listCmd(providers []provider.Provider, cachePath string, timeout func(provider.Kind) time.Duration, theme styles.Styles, withIcons bool, args []string, out, errOut io.Writer) error {
|
||||
fs := flag.NewFlagSet("list", flag.ContinueOnError)
|
||||
cached := fs.Bool("cached", false, "only what's cached, and repos: load nothing from Azure or Databricks")
|
||||
maxAge := fs.Duration("max-age", 2*time.Minute, "load cached lists older than this again")
|
||||
format := fs.String("format", "text", "text: link, kind and name, tab-separated; json: an object per line")
|
||||
color := fs.String("color", "always", "colour the names (text): always or never; fzf needs --ansi")
|
||||
iconsOn := fs.Bool("icons", withIcons, "put the provider's Nerd Font icon before names (text)")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return err
|
||||
}
|
||||
if fs.NArg() > 0 {
|
||||
return fmt.Errorf("usage: lazysecrets list [--cached] [--max-age 2m] [--format text|json]")
|
||||
}
|
||||
if *color != "always" && *color != "never" {
|
||||
return fmt.Errorf("--color: %q is not always or never", *color)
|
||||
}
|
||||
iconSet := icons.None
|
||||
if *iconsOn {
|
||||
iconSet = icons.NerdFont
|
||||
}
|
||||
var write func(listing.Entry) error
|
||||
switch *format {
|
||||
case "text":
|
||||
write = func(e listing.Entry) error {
|
||||
name := strings.Join(e.Path, " / ")
|
||||
if e.Account != "" {
|
||||
name += " · " + e.Account
|
||||
}
|
||||
_, err := fmt.Fprintf(out, "%s\t%s\t%s\n", e.Link, e.Kind, name)
|
||||
_, err := fmt.Fprintf(out, "%s\t%s\t%s\n", e.Link, e.Kind, nameOf(e, theme, iconSet, *color == "always"))
|
||||
return err
|
||||
}
|
||||
case "json":
|
||||
@@ -95,3 +107,32 @@ func listCmd(providers []provider.Provider, cachePath string, timeout func(provi
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// nameOf is an entry's name for the text format: the provider's icon,
|
||||
// the path with the item's own name standing out, and the account.
|
||||
func nameOf(e listing.Entry, theme styles.Styles, iconSet icons.Set, color bool) string {
|
||||
render := func(st lipgloss.Style, s string) string {
|
||||
if !color || s == "" {
|
||||
return s
|
||||
}
|
||||
return st.Render(s)
|
||||
}
|
||||
kind := provider.Kind("")
|
||||
for _, k := range []provider.Kind{provider.KindAzure, provider.KindDatabricks, provider.KindRepo} {
|
||||
if strings.EqualFold(string(k), e.Provider) {
|
||||
kind = k
|
||||
}
|
||||
}
|
||||
icon := iconSet.Prefix(kind)
|
||||
var name string
|
||||
if n := len(e.Path); n > 0 {
|
||||
if n > 1 {
|
||||
name = render(theme.Muted, strings.Join(e.Path[:n-1], " / ")+" / ")
|
||||
}
|
||||
name += e.Path[n-1]
|
||||
}
|
||||
if e.Account != "" {
|
||||
name += render(theme.Muted, " · "+e.Account)
|
||||
}
|
||||
return render(theme.Kind(kind), strings.TrimSuffix(icon, " ")) + strings.Repeat(" ", min(len(icon), 1)) + name
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.kebcloud.no/keb/lazysecrets/internal/listing"
|
||||
"git.kebcloud.no/keb/lazysecrets/internal/ui/icons"
|
||||
"git.kebcloud.no/keb/lazysecrets/internal/ui/styles"
|
||||
)
|
||||
|
||||
func TestListNames(t *testing.T) {
|
||||
e := listing.Entry{Provider: "azure", Path: []string{"platform-prod", "kv-app-prod", "db-password"}, Account: "alice"}
|
||||
if got := nameOf(e, styles.Default(), icons.None, false); got != "platform-prod / kv-app-prod / db-password · alice" {
|
||||
t.Errorf("plain: %q", got)
|
||||
}
|
||||
got := nameOf(e, styles.Default(), icons.NerdFont, true)
|
||||
if !strings.HasPrefix(got, "\x1b[") || !strings.Contains(got, icons.NerdFont.Azure) {
|
||||
t.Errorf("no colour or icon: %q", got)
|
||||
}
|
||||
// The item's own name isn't muted with its path.
|
||||
if !strings.Contains(got, "\x1b[mdb-password") {
|
||||
t.Errorf("name not set apart: %q", got)
|
||||
}
|
||||
}
|
||||
@@ -122,6 +122,7 @@ func run() error {
|
||||
lazysecrets [flags] get <ref> print the value a lazysecrets:// link points at,
|
||||
for scripts: VAR=$(lazysecrets get <ref>)
|
||||
lazysecrets [flags] list [--cached] [--max-age 2m] [--format text|json]
|
||||
[--color always|never] [--icons=false]
|
||||
print every source, vault and secret it can
|
||||
find, one per line, for fzf and pickers:
|
||||
what's cached at once, then what loads
|
||||
@@ -356,7 +357,7 @@ Flags:
|
||||
if d, err := config.CacheDir(); err == nil && !*demo && config.On(cfg.Storage.Cache) {
|
||||
cachePath = filepath.Join(d, "cache.json")
|
||||
}
|
||||
return listCmd(providers, cachePath, timeoutFor, listArgs, os.Stdout, os.Stderr)
|
||||
return listCmd(providers, cachePath, timeoutFor, theme, *icons, listArgs, os.Stdout, os.Stderr)
|
||||
}
|
||||
if shareArgs != nil {
|
||||
// The files from a repo, the key from whichever provider it's
|
||||
|
||||
+8
-2
@@ -144,7 +144,7 @@ and its name, tab-separated. It's what Go to searches, so fzf, or an
|
||||
editor's picker, can search it instead:
|
||||
|
||||
```sh
|
||||
lazysecrets list | fzf --delimiter='\t' --with-nth=3 \
|
||||
lazysecrets list | fzf --ansi --delimiter='\t' --with-nth=3 \
|
||||
--bind 'enter:become(lazysecrets open {1})' \
|
||||
--bind 'ctrl-y:execute-silent(lazysecrets get {1} | pbcopy)'
|
||||
```
|
||||
@@ -156,7 +156,13 @@ Azure or Databricks, for an answer at once. Repos are always read, from
|
||||
disk. What it loads is cached for the TUI too. Lists it can't read are
|
||||
reported on stderr; it stops when the reader goes, after a pick in fzf.
|
||||
|
||||
`--format json` prints an object per line instead, for editor plugins:
|
||||
The name is coloured from your [theme](config.md#theme), with the
|
||||
provider's icon when [icons](config.md#theme) are on (`ui.icons`); fzf
|
||||
shows the colours with `--ansi`. `--color never` leaves them out, for
|
||||
scripts, and `--icons=false` the icons. The link is never coloured.
|
||||
|
||||
`--format json` prints an object per line instead, for editor plugins,
|
||||
without colours or icons:
|
||||
|
||||
```json
|
||||
{"link":"lazysecrets://azure/kv-app-prod.vault.azure.net/secrets/db-password","kind":"secret","provider":"azure","path":["platform-prod","kv-app-prod","db-password"],"account":"alice"}
|
||||
|
||||
Reference in new issue
Block a user