feat: lazysecrets list prints everything lazysecrets can find, for fzf and editor pickers #70
No files matched your search
+38
-16
@@ -41,7 +41,7 @@ import (
|
|||||||
// follows as it loads. A list that can't be read is a line too, as it
|
// follows as it loads. A list that can't be read is a line too, as it
|
||||||
// fails: the vault or source, then ✗ and why in the theme's error colour.
|
// fails: the vault or source, then ✗ and why in the theme's error colour.
|
||||||
// They're in the output rather than on stderr, which would draw over fzf.
|
// They're in the output rather than on stderr, which would draw over fzf.
|
||||||
func listCmd(providers []provider.Provider, st *store.Store, cachePath string, timeout func(provider.Kind) time.Duration, theme styles.Styles, withIcons, withRecent bool, args []string, out io.Writer) error {
|
func listCmd(providers []provider.Provider, st *store.Store, cachePath string, timeout func(provider.Kind) time.Duration, theme styles.Styles, withIcons, withRecent bool, expiryWarn time.Duration, args []string, out io.Writer) error {
|
||||||
fs := flag.NewFlagSet("list", flag.ContinueOnError)
|
fs := flag.NewFlagSet("list", flag.ContinueOnError)
|
||||||
cached := fs.Bool("cached", false, "only what's cached, and repos: load nothing from Azure or Databricks")
|
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")
|
maxAge := fs.Duration("max-age", 2*time.Minute, "load cached lists older than this again")
|
||||||
@@ -50,6 +50,7 @@ func listCmd(providers []provider.Provider, st *store.Store, cachePath string, t
|
|||||||
iconsOn := fs.Bool("icons", withIcons, "put the provider's Nerd Font icon before names (text)")
|
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)")
|
stars := fs.Bool("stars", true, "put ★ before starred names, in the theme's favouriteColor (text)")
|
||||||
recent := fs.Bool("recent", withRecent, "put ◷ before names used lately, in mutedColor (text; default ui.recentIcon)")
|
recent := fs.Bool("recent", withRecent, "put ◷ before names used lately, in mutedColor (text; default ui.recentIcon)")
|
||||||
|
expiry := fs.Bool("expiry", true, "put ⚠ before names expiring within ui.expiryWarningDays, ✗ before expired ones (text)")
|
||||||
kinds := fs.String("kind", "", "only these kinds, comma-separated: "+strings.Join(listing.Kinds, ", ")+" (vault: also scopes and repo files)")
|
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\")")
|
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 {
|
if err := fs.Parse(args); err != nil {
|
||||||
@@ -71,16 +72,17 @@ func listCmd(providers []provider.Provider, st *store.Store, cachePath string, t
|
|||||||
if *color != "always" && *color != "never" {
|
if *color != "always" && *color != "never" {
|
||||||
return fmt.Errorf("--color: %q is not always or never", *color)
|
return fmt.Errorf("--color: %q is not always or never", *color)
|
||||||
}
|
}
|
||||||
iconSet := icons.None
|
m := marks{theme: theme, icons: icons.None, stars: *stars, recent: *recent, expiry: *expiry,
|
||||||
|
color: *color == "always", warn: expiryWarn, now: time.Now()}
|
||||||
if *iconsOn {
|
if *iconsOn {
|
||||||
iconSet = icons.NerdFont
|
m.icons = icons.NerdFont
|
||||||
}
|
}
|
||||||
var write func(listing.Entry) error
|
var write func(listing.Entry) error
|
||||||
switch *format {
|
switch *format {
|
||||||
case "text":
|
case "text":
|
||||||
write = func(e listing.Entry) error {
|
write = func(e listing.Entry) error {
|
||||||
colored := *color == "always"
|
colored := *color == "always"
|
||||||
mark := markOf(e, theme, iconSet, *stars, *recent, colored)
|
mark := m.of(e)
|
||||||
name := nameOf(e, theme, colored)
|
name := nameOf(e, theme, colored)
|
||||||
why := errorOf(e, theme, colored)
|
why := errorOf(e, theme, colored)
|
||||||
// A tab or line break in a name would break the line up.
|
// A tab or line break in a name would break the line up.
|
||||||
@@ -165,12 +167,21 @@ func nameOf(e listing.Entry, theme styles.Styles, color bool) string {
|
|||||||
return name
|
return name
|
||||||
}
|
}
|
||||||
|
|
||||||
// markOf is what goes before the name, in a field of its own so a search
|
// marks says what goes before the name, in a field of its own so a
|
||||||
// leaves it out: ★ for a starred entry and ◷ for one used lately (as the
|
// search leaves it out: ★ for a starred entry, ◷ for one used lately,
|
||||||
// TUI marks them; a space as wide for the rest, so names line up), and
|
// ⚠ for one expiring within warn and ✗ for an expired one (as the TUI
|
||||||
// the provider's icon.
|
// marks them; a space as wide where it isn't, so names line up), then the
|
||||||
func markOf(e listing.Entry, theme styles.Styles, iconSet icons.Set, stars, recent, color bool) string {
|
// provider's icon.
|
||||||
render := renderer(color)
|
type marks struct {
|
||||||
|
theme styles.Styles
|
||||||
|
icons icons.Set
|
||||||
|
stars, recent, expiry, color bool
|
||||||
|
warn time.Duration // ui.expiryWarningDays
|
||||||
|
now time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m marks) of(e listing.Entry) string {
|
||||||
|
render := renderer(m.color)
|
||||||
var parts []string
|
var parts []string
|
||||||
mark := func(on bool, st lipgloss.Style, s string) {
|
mark := func(on bool, st lipgloss.Style, s string) {
|
||||||
if !on {
|
if !on {
|
||||||
@@ -178,11 +189,22 @@ func markOf(e listing.Entry, theme styles.Styles, iconSet icons.Set, stars, rece
|
|||||||
}
|
}
|
||||||
parts = append(parts, render(st, s))
|
parts = append(parts, render(st, s))
|
||||||
}
|
}
|
||||||
if stars {
|
if m.stars {
|
||||||
mark(e.Starred, theme.Favourite, "★")
|
mark(e.Starred, m.theme.Favourite, "★")
|
||||||
}
|
}
|
||||||
if recent {
|
if m.recent {
|
||||||
mark(e.Recent, theme.Muted, "◷")
|
mark(e.Recent, m.theme.Muted, "◷")
|
||||||
|
}
|
||||||
|
if m.expiry {
|
||||||
|
left := e.Expires.Sub(m.now)
|
||||||
|
switch {
|
||||||
|
case e.Expires.IsZero() || left > m.warn:
|
||||||
|
mark(false, m.theme.Muted, "")
|
||||||
|
case left <= 0:
|
||||||
|
mark(true, m.theme.Error, "✗")
|
||||||
|
default:
|
||||||
|
mark(true, m.theme.Warning, "⚠")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
kind := provider.Kind("")
|
kind := provider.Kind("")
|
||||||
for _, k := range []provider.Kind{provider.KindAzure, provider.KindDatabricks, provider.KindRepo} {
|
for _, k := range []provider.Kind{provider.KindAzure, provider.KindDatabricks, provider.KindRepo} {
|
||||||
@@ -190,8 +212,8 @@ func markOf(e listing.Entry, theme styles.Styles, iconSet icons.Set, stars, rece
|
|||||||
kind = k
|
kind = k
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if icon := strings.TrimSpace(iconSet.Prefix(kind)); icon != "" {
|
if icon := strings.TrimSpace(m.icons.Prefix(kind)); icon != "" {
|
||||||
parts = append(parts, render(theme.Kind(kind), icon))
|
parts = append(parts, render(m.theme.Kind(kind), icon))
|
||||||
}
|
}
|
||||||
return strings.Join(parts, " ")
|
return strings.Join(parts, " ")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"git.kebcloud.no/keb/lazysecrets/internal/listing"
|
"git.kebcloud.no/keb/lazysecrets/internal/listing"
|
||||||
"git.kebcloud.no/keb/lazysecrets/internal/ui/icons"
|
"git.kebcloud.no/keb/lazysecrets/internal/ui/icons"
|
||||||
@@ -22,39 +23,54 @@ func TestListNames(t *testing.T) {
|
|||||||
|
|
||||||
func TestListMarks(t *testing.T) {
|
func TestListMarks(t *testing.T) {
|
||||||
az := icons.NerdFont.Azure
|
az := icons.NerdFont.Azure
|
||||||
|
plain := marks{theme: styles.Default(), icons: icons.NerdFont, stars: true}
|
||||||
|
noIcons := plain
|
||||||
|
noIcons.icons = icons.None
|
||||||
|
colored := noIcons
|
||||||
|
colored.color = true
|
||||||
for _, c := range []struct {
|
for _, c := range []struct {
|
||||||
e listing.Entry
|
m marks
|
||||||
stars, color bool
|
e listing.Entry
|
||||||
set icons.Set
|
want string
|
||||||
want string
|
|
||||||
}{
|
}{
|
||||||
{listing.Entry{Provider: "azure", Starred: true}, true, false, icons.NerdFont, "★ " + az},
|
{plain, listing.Entry{Provider: "azure", Starred: true}, "★ " + az},
|
||||||
{listing.Entry{Provider: "azure"}, true, false, icons.NerdFont, " " + az}, // as wide as ★
|
{plain, listing.Entry{Provider: "azure"}, " " + az}, // as wide as ★
|
||||||
{listing.Entry{Provider: "azure", Starred: true}, false, false, icons.NerdFont, az},
|
{marks{icons: icons.NerdFont}, listing.Entry{Provider: "azure", Starred: true}, az},
|
||||||
{listing.Entry{Provider: "azure", Starred: true}, true, false, icons.None, "★"},
|
{noIcons, listing.Entry{Provider: "azure", Starred: true}, "★"},
|
||||||
{listing.Entry{Provider: "azure", Starred: true}, false, false, icons.None, ""},
|
{marks{}, listing.Entry{Provider: "azure", Starred: true}, ""},
|
||||||
{listing.Entry{Provider: "azure", Starred: true}, true, true, icons.None, styles.Default().Favourite.Render("★")},
|
{colored, listing.Entry{Provider: "azure", Starred: true}, styles.Default().Favourite.Render("★")},
|
||||||
} {
|
} {
|
||||||
if got := markOf(c.e, styles.Default(), c.set, c.stars, false, c.color); got != c.want {
|
if got := c.m.of(c.e); 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)
|
t.Errorf("%+v: %q, want %q", c.e, got, c.want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestListRecentMark(t *testing.T) {
|
func TestListRecentAndExpiryMarks(t *testing.T) {
|
||||||
az := icons.NerdFont.Azure
|
az := icons.NerdFont.Azure
|
||||||
|
now := time.Date(2026, 10, 14, 12, 0, 0, 0, time.UTC)
|
||||||
|
m := marks{icons: icons.NerdFont, stars: true, recent: true, expiry: true, warn: 30 * 24 * time.Hour, now: now}
|
||||||
for _, c := range []struct {
|
for _, c := range []struct {
|
||||||
e listing.Entry
|
e listing.Entry
|
||||||
want string
|
want string
|
||||||
}{
|
}{
|
||||||
{listing.Entry{Provider: "azure", Starred: true, Recent: true}, "★ ◷ " + az},
|
{listing.Entry{Provider: "azure", Starred: true, Recent: true}, "★ ◷ " + az},
|
||||||
{listing.Entry{Provider: "azure", Recent: true}, " ◷ " + az},
|
{listing.Entry{Provider: "azure", Recent: true}, " ◷ " + az},
|
||||||
{listing.Entry{Provider: "azure", Starred: true}, "★ " + az}, // as wide as ◷
|
{listing.Entry{Provider: "azure", Expires: now.Add(-time.Hour)}, " ✗ " + az},
|
||||||
|
{listing.Entry{Provider: "azure", Expires: now.Add(10 * 24 * time.Hour)}, " ⚠ " + az},
|
||||||
|
{listing.Entry{Provider: "azure", Expires: now.Add(60 * 24 * time.Hour)}, " " + az},
|
||||||
} {
|
} {
|
||||||
if got := markOf(c.e, styles.Default(), icons.NerdFont, true, true, false); got != c.want {
|
if got := m.of(c.e); got != c.want {
|
||||||
t.Errorf("markOf(starred %v, recent %v) = %q, want %q", c.e.Starred, c.e.Recent, got, c.want)
|
t.Errorf("%+v: %q, want %q", c.e, got, c.want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
m.color, m.theme = true, styles.Default()
|
||||||
|
if got := m.of(listing.Entry{Expires: now.Add(-time.Hour)}); !strings.Contains(got, styles.Default().Error.Render("✗")) {
|
||||||
|
t.Errorf("expired isn't a red ✗: %q", got)
|
||||||
|
}
|
||||||
|
if got := m.of(listing.Entry{Expires: now.Add(time.Hour)}); !strings.Contains(got, styles.Default().Warning.Render("⚠")) {
|
||||||
|
t.Errorf("expiring isn't a ⚠ in the warning colour: %q", got)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestListErrors(t *testing.T) {
|
func TestListErrors(t *testing.T) {
|
||||||
|
|||||||
@@ -389,7 +389,7 @@ Flags:
|
|||||||
if d, err := config.CacheDir(); err == nil && !*demo && config.On(cfg.Storage.Cache) {
|
if d, err := config.CacheDir(); err == nil && !*demo && config.On(cfg.Storage.Cache) {
|
||||||
cachePath = filepath.Join(d, "cache.json")
|
cachePath = filepath.Join(d, "cache.json")
|
||||||
}
|
}
|
||||||
return listCmd(providers, st, cachePath, timeoutFor, theme, *icons, recentIcon, listArgs, os.Stdout)
|
return listCmd(providers, st, cachePath, timeoutFor, theme, *icons, recentIcon, time.Duration(expiryDays)*24*time.Hour, listArgs, os.Stdout)
|
||||||
}
|
}
|
||||||
if shareArgs != nil {
|
if shareArgs != nil {
|
||||||
// The files from a repo, the key from whichever provider it's
|
// The files from a repo, the key from whichever provider it's
|
||||||
|
|||||||
+1
-1
@@ -13,7 +13,7 @@ ui:
|
|||||||
sourcesView: grouped # or flat; ` toggles it
|
sourcesView: grouped # or flat; ` toggles it
|
||||||
collapseGroups: true # start with the Sources pane's groups folded; enter or space unfolds one
|
collapseGroups: true # start with the Sources pane's groups folded; enter or space unfolds one
|
||||||
loadDelay: 150ms # how long the cursor rests on a row before it loads; 0s loads at once
|
loadDelay: 150ms # how long the cursor rests on a row before it loads; 0s loads at once
|
||||||
expiryWarningDays: 30 # mark secrets, keys and certificates expiring this soon (⚠ 12d); expired ones get ✗
|
expiryWarningDays: 30 # mark secrets, keys and certificates expiring this soon (⚠ 12d; ⚠ in lazysecrets list); expired ones get ✗
|
||||||
values:
|
values:
|
||||||
revealTimeout: 60s # hide a revealed value after this long; 0s keeps it shown
|
revealTimeout: 60s # hide a revealed value after this long; 0s keeps it shown
|
||||||
hideOnBlur: true # hide a revealed value when the terminal loses focus (tmux: focus-events on)
|
hideOnBlur: true # hide a revealed value when the terminal loses focus (tmux: focus-events on)
|
||||||
|
|||||||
@@ -45,6 +45,10 @@ type Entry struct {
|
|||||||
Recent bool `json:"recent"`
|
Recent bool `json:"recent"`
|
||||||
LastUsed time.Time `json:"lastUsed,omitzero"`
|
LastUsed time.Time `json:"lastUsed,omitzero"`
|
||||||
Uses int `json:"uses,omitempty"`
|
Uses int `json:"uses,omitempty"`
|
||||||
|
// Expires is when a secret, key or certificate expires, if it does;
|
||||||
|
// Enabled whether it's enabled, where the provider says (Azure).
|
||||||
|
Expires time.Time `json:"expires,omitzero"`
|
||||||
|
Enabled *bool `json:"enabled,omitempty"`
|
||||||
// Error says why a source's vaults or a vault's secrets couldn't be
|
// Error says why a source's vaults or a vault's secrets couldn't be
|
||||||
// listed. The entry is then that source or vault, again, or with an
|
// listed. The entry is then that source or vault, again, or with an
|
||||||
// account's sources unreadable, the account without a link.
|
// account's sources unreadable, the account without a link.
|
||||||
@@ -200,7 +204,8 @@ func (l *lister) objects(p provider.Provider, c provider.Container, objs []provi
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
l.put(p, Entry{Link: r.String(), Kind: strings.ToLower(string(o.Type)), Provider: kindOf(c.Source.Kind),
|
l.put(p, Entry{Link: r.String(), Kind: strings.ToLower(string(o.Type)), Provider: kindOf(c.Source.Kind),
|
||||||
Path: []string{c.Source.Name, c.Name, o.Name}, AccountLabel: accountOf(c.Source)})
|
Path: []string{c.Source.Name, c.Name, o.Name}, AccountLabel: accountOf(c.Source),
|
||||||
|
Expires: o.Expires, Enabled: o.Enabled})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user