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
2 changed files with 76 additions and 61 deletions
Showing only changes of commit 46af233bd3 - Show all commits

No files matched your search

+51 -41
View File
@@ -27,12 +27,13 @@ import (
// listCmd prints every source, vault and secret lazysecrets can find, one
// per line, for fzf, editor pickers and scripts:
//
// lazysecrets list | fzf --ansi --with-nth='2..-3' --accept-nth=-1
// lazysecrets list | fzf --ansi --delimiter='\t' --with-nth='2..-3' --nth='2..' --accept-nth=-1
//
// Each line is the kind, the name, why it couldn't be read (empty unless
// it failed), the account and the link, tab-separated: kind, account and
// link are one word each, so even fields split on spaces keep them first
// and last two. The name is coloured from the theme,
// Each line is the kind, a mark (★ and the provider's icon), the name,
// why it couldn't be read (empty unless it failed), the account and the
// link, tab-separated. The mark has a field of its own so a search can
// leave it out (fzf --nth), and ^prod finds names starting with prod. 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.
//
@@ -44,7 +45,7 @@ func listCmd(providers []provider.Provider, st *store.Store, cachePath string, t
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: kind, name, error, account and link, tab-separated; json: an object per line")
format := fs.String("format", "text", "text: kind, mark, name, error, account 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)")
stars := fs.Bool("stars", true, "put ★ before starred names, in the theme's favouriteColor (text)")
@@ -77,13 +78,12 @@ func listCmd(providers []provider.Provider, st *store.Store, cachePath string, t
switch *format {
case "text":
write = func(e listing.Entry) error {
colored := *color == "always"
mark := markOf(e, theme, iconSet, *stars, colored)
name := nameOf(e, theme, colored)
why := errorOf(e, theme, colored)
// A tab or line break in a name would break the line up.
name := nameOf(e, theme, iconSet, *color == "always")
if *stars {
name = starOf(e, theme, *color == "always") + name
}
why := errorOf(e, theme, *color == "always")
_, err := fmt.Fprintf(out, "%s\t%s\t%s\t%s\t%s\n", e.Kind, oneLine.Replace(name), oneLine.Replace(why), e.Account, e.Link)
_, err := fmt.Fprintf(out, "%s\t%s\t%s\t%s\t%s\t%s\n", e.Kind, mark, oneLine.Replace(name), oneLine.Replace(why), e.Account, e.Link)
return err
}
case "json":
@@ -138,18 +138,6 @@ func listCmd(providers []provider.Provider, st *store.Store, cachePath string, t
var oneLine = strings.NewReplacer("\t", " ", "\r\n", " ", "\n", " ", "\r", " ")
// starOf is ★ for a starred entry, as the TUI marks them, and spaces
// the same width for the rest, so names line up.
func starOf(e listing.Entry, theme styles.Styles, color bool) string {
switch {
case !e.Starred:
return " "
case color:
return theme.Favourite.Render("★") + " "
}
return "★ "
}
// errorOf is why a list couldn't be read, or "": with colour, a red ✗
// before it, as the TUI marks lists it can't read.
func errorOf(e listing.Entry, theme styles.Styles, color bool) string {
@@ -159,22 +147,10 @@ func errorOf(e listing.Entry, theme styles.Styles, color bool) string {
return theme.Error.Render("✗ " + e.Error)
}
// 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)
// nameOf is an entry's name for the text format: the path with the
// item's own name standing out, and the account.
func nameOf(e listing.Entry, theme styles.Styles, color bool) string {
render := renderer(color)
var name string
if n := len(e.Path); n > 0 {
if n > 1 {
@@ -185,5 +161,39 @@ func nameOf(e listing.Entry, theme styles.Styles, iconSet icons.Set, color bool)
if e.AccountLabel != "" {
name += render(theme.Muted, " · "+e.AccountLabel)
}
return render(theme.Kind(kind), strings.TrimSuffix(icon, " ")) + strings.Repeat(" ", min(len(icon), 1)) + name
return name
}
// markOf is what goes before the name, in a field of its own so a search
// leaves it out: ★ for a starred entry (as the TUI marks them; a space
// as wide for the rest, so names line up), and the provider's icon.
func markOf(e listing.Entry, theme styles.Styles, iconSet icons.Set, stars, color bool) string {
render := renderer(color)
var parts []string
if stars {
star := " "
if e.Starred {
star = render(theme.Favourite, "★")
}
parts = append(parts, star)
}
kind := provider.Kind("")
for _, k := range []provider.Kind{provider.KindAzure, provider.KindDatabricks, provider.KindRepo} {
if strings.EqualFold(string(k), e.Provider) {
kind = k
}
}
if icon := strings.TrimSpace(iconSet.Prefix(kind)); icon != "" {
parts = append(parts, render(theme.Kind(kind), icon))
}
return strings.Join(parts, " ")
}
func renderer(color bool) func(lipgloss.Style, string) string {
return func(st lipgloss.Style, s string) string {
if !color || s == "" {
return s
}
return st.Render(s)
}
}
+25 -20
View File
@@ -11,22 +11,39 @@ import (
func TestListNames(t *testing.T) {
e := listing.Entry{Provider: "azure", Path: []string{"platform-prod", "kv-app-prod", "db-password"}, AccountLabel: "alice"}
if got := nameOf(e, styles.Default(), icons.None, false); got != "platform-prod / kv-app-prod / db-password · alice" {
if got := nameOf(e, styles.Default(), 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") {
if got := nameOf(e, styles.Default(), true); !strings.Contains(got, "\x1b[mdb-password") {
t.Errorf("name not set apart: %q", got)
}
}
func TestListErrorNames(t *testing.T) {
func TestListMarks(t *testing.T) {
az := icons.NerdFont.Azure
for _, c := range []struct {
e listing.Entry
stars, color bool
set icons.Set
want string
}{
{listing.Entry{Provider: "azure", Starred: true}, true, false, icons.NerdFont, "★ " + az},
{listing.Entry{Provider: "azure"}, true, false, icons.NerdFont, " " + az}, // as wide as ★
{listing.Entry{Provider: "azure", Starred: true}, false, false, icons.NerdFont, az},
{listing.Entry{Provider: "azure", Starred: true}, true, false, icons.None, "★"},
{listing.Entry{Provider: "azure", Starred: true}, false, false, icons.None, ""},
{listing.Entry{Provider: "azure", Starred: true}, true, true, icons.None, styles.Default().Favourite.Render("★")},
} {
if got := markOf(c.e, styles.Default(), c.set, c.stars, c.color); got != c.want {
t.Errorf("markOf(starred %v, stars %v, colour %v) = %q, want %q", c.e.Starred, c.stars, c.color, got, c.want)
}
}
}
func TestListErrors(t *testing.T) {
e := listing.Entry{Provider: "azure", Path: []string{"platform-prod", "kv-restricted"}, Error: "403 Forbidden"}
if got := nameOf(e, styles.Default(), icons.NerdFont, false); got != icons.NerdFont.Azure+" platform-prod / kv-restricted" {
if got := nameOf(e, styles.Default(), false); got != "platform-prod / kv-restricted" {
t.Errorf("the name has more: %q", got)
}
if got := errorOf(e, styles.Default(), false); got != "403 Forbidden" {
@@ -39,15 +56,3 @@ func TestListErrorNames(t *testing.T) {
t.Errorf("no error: %q", got)
}
}
func TestListStars(t *testing.T) {
if got := starOf(listing.Entry{Starred: true}, styles.Default(), false); got != "★ " {
t.Errorf("starred: %q", got)
}
if got := starOf(listing.Entry{}, styles.Default(), true); got != " " {
t.Errorf("not starred: %q, want spaces as wide as ★", got)
}
if got := starOf(listing.Entry{Starred: true}, styles.Default(), true); got != styles.Default().Favourite.Render("★")+" " {
t.Errorf("not in the favourite colour: %q", got)
}
}