feat: lazysecrets list shows unreadable lists as red ✗ lines, and filters by kind

This commit is contained in:
keb committed 2026-10-14 21:24:01 +02:00
1 parent baee1dfe61
commit 7b377fd468
6 files changed
+197 -81

No files matched your search

+36 -23
View File
@@ -9,6 +9,7 @@ import (
"io"
"os"
"os/signal"
"slices"
"strings"
"syscall"
"time"
@@ -32,26 +33,33 @@ import (
// 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 to errOut:
// stderr when the output is on the terminal, nothing when it goes into
// fzf, whose screen they'd draw over; --errors reports them to stderr
// either way.
func listCmd(providers []provider.Provider, cachePath string, timeout func(provider.Kind) time.Duration, theme styles.Styles, withIcons bool, args []string, out, errOut, stderr io.Writer) error {
// follows as it loads. A list that can't be read is a line too, as it
// fails: ✗, the vault or source, and why, in the theme's error colour.
// They're in the output rather than on stderr, which would draw over fzf.
func listCmd(providers []provider.Provider, cachePath string, timeout func(provider.Kind) time.Duration, theme styles.Styles, withIcons bool, args []string, out 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: name and link, 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)")
showErrors := fs.Bool("errors", false, "report lists that can't be read on stderr also when the output goes into a pipe")
kinds := fs.String("kind", "", "only these kinds, comma-separated: "+strings.Join(listing.Kinds, ", ")+" (vault: also scopes and repo files)")
showErrors := fs.Bool("errors", true, "list the lists that can't be read, with why (text: ✗ lines; json: \"error\")")
if err := fs.Parse(args); err != nil {
return err
}
if *showErrors {
errOut = stderr
}
if fs.NArg() > 0 {
return fmt.Errorf("usage: lazysecrets list [--cached] [--max-age 2m] [--format text|json]")
return fmt.Errorf("usage: lazysecrets list [--kind secret,…] [--cached] [--max-age 2m] [--format text|json]")
}
var only []string
for k := range strings.SplitSeq(*kinds, ",") {
if k = strings.TrimSpace(k); k == "" {
continue
}
if !slices.Contains(listing.Kinds, k) {
return fmt.Errorf("--kind: %q is not one of %s", k, strings.Join(listing.Kinds, ", "))
}
only = append(only, k)
}
if *color != "always" && *color != "never" {
return fmt.Errorf("--color: %q is not always or never", *color)
@@ -80,6 +88,9 @@ func listCmd(providers []provider.Provider, cachePath string, timeout func(provi
signal.Notify(make(chan os.Signal, 1), syscall.SIGPIPE)
var gone error
emit := func(e listing.Entry) error {
if e.Error != "" && !*showErrors {
return nil
}
if err := write(e); err != nil {
gone = err
return err
@@ -91,8 +102,8 @@ func listCmd(providers []provider.Provider, cachePath string, timeout func(provi
if cachePath != "" {
f = cachefile.Read(cachePath)
}
f, errs := listing.Run(context.Background(), listing.Options{
Providers: providers, Cache: f, MaxAge: *maxAge, Cached: *cached, Timeout: timeout,
f = listing.Run(context.Background(), listing.Options{
Providers: providers, Cache: f, MaxAge: *maxAge, Cached: *cached, Kinds: only, Timeout: timeout,
}, emit)
if cachePath != "" && !*cached {
byID := map[string]provider.Provider{}
@@ -101,23 +112,18 @@ func listCmd(providers []provider.Provider, cachePath string, timeout func(provi
}
keep := func(id string) bool { p, ok := byID[id]; return ok && p.Kind() != provider.KindRepo }
if err := cachefile.Save(cachePath, f, keep, time.Now()); err != nil {
errs = append(errs, err)
return err
}
}
if gone != nil {
if errors.Is(gone, syscall.EPIPE) {
return nil // the reader has what it wanted
}
if gone != nil && !errors.Is(gone, syscall.EPIPE) {
return gone
}
for _, err := range errs {
fmt.Fprintln(errOut, "lazysecrets: list:", err)
}
return nil
return nil // or the reader has what it wanted
}
// 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.
// the path with the item's own name standing out, and the account. A
// list that couldn't be read has ✗ for the icon, and why after it.
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 == "" {
@@ -132,6 +138,10 @@ func nameOf(e listing.Entry, theme styles.Styles, iconSet icons.Set, color bool)
}
}
icon := iconSet.Prefix(kind)
iconStyle := theme.Kind(kind)
if e.Error != "" {
icon, iconStyle = "✗ ", theme.Error
}
var name string
if n := len(e.Path); n > 0 {
if n > 1 {
@@ -142,5 +152,8 @@ func nameOf(e listing.Entry, theme styles.Styles, iconSet icons.Set, color bool)
if e.Account != "" {
name += render(theme.Muted, " · "+e.Account)
}
return render(theme.Kind(kind), strings.TrimSuffix(icon, " ")) + strings.Repeat(" ", min(len(icon), 1)) + name
if e.Error != "" {
name += " " + render(theme.Error, e.Error)
}
return render(iconStyle, strings.TrimSuffix(icon, " ")) + strings.Repeat(" ", min(len(icon), 1)) + name
}