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 83 additions and 10 deletions
Showing only changes of commit e99462bd21 - Show all commits

No files matched your search

+48 -7
View File
@@ -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
}
+25
View File
@@ -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)
}
}
+2 -1
View File
@@ -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
View File
@@ -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"}