feat: lazysecrets list prints everything lazysecrets can find, for fzf and editor pickers #70
No files matched your search
+17
-10
@@ -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 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, 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")
|
||||||
@@ -49,6 +49,7 @@ func listCmd(providers []provider.Provider, st *store.Store, cachePath string, t
|
|||||||
color := fs.String("color", "always", "colour the names (text): always or never; fzf needs --ansi")
|
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)")
|
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)")
|
||||||
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 {
|
||||||
@@ -79,7 +80,7 @@ func listCmd(providers []provider.Provider, st *store.Store, cachePath string, t
|
|||||||
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, colored)
|
mark := markOf(e, theme, iconSet, *stars, *recent, colored)
|
||||||
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,17 +166,23 @@ func nameOf(e listing.Entry, theme styles.Styles, color bool) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// markOf is what goes before the name, in a field of its own so a search
|
// 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
|
// leaves it out: ★ for a starred entry and ◷ for one used lately (as the
|
||||||
// as wide for the rest, so names line up), and the provider's icon.
|
// TUI marks them; a space as wide for the rest, so names line up), and
|
||||||
func markOf(e listing.Entry, theme styles.Styles, iconSet icons.Set, stars, color bool) string {
|
// the provider's icon.
|
||||||
|
func markOf(e listing.Entry, theme styles.Styles, iconSet icons.Set, stars, recent, color bool) string {
|
||||||
render := renderer(color)
|
render := renderer(color)
|
||||||
var parts []string
|
var parts []string
|
||||||
if stars {
|
mark := func(on bool, st lipgloss.Style, s string) {
|
||||||
star := " "
|
if !on {
|
||||||
if e.Starred {
|
s = " "
|
||||||
star = render(theme.Favourite, "★")
|
|
||||||
}
|
}
|
||||||
parts = append(parts, star)
|
parts = append(parts, render(st, s))
|
||||||
|
}
|
||||||
|
if stars {
|
||||||
|
mark(e.Starred, theme.Favourite, "★")
|
||||||
|
}
|
||||||
|
if recent {
|
||||||
|
mark(e.Recent, theme.Muted, "◷")
|
||||||
}
|
}
|
||||||
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} {
|
||||||
|
|||||||
@@ -35,12 +35,28 @@ func TestListMarks(t *testing.T) {
|
|||||||
{listing.Entry{Provider: "azure", Starred: true}, false, 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("★")},
|
{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 {
|
if got := markOf(c.e, styles.Default(), c.set, c.stars, false, 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)
|
t.Errorf("markOf(starred %v, stars %v, colour %v) = %q, want %q", c.e.Starred, c.stars, c.color, got, c.want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestListRecentMark(t *testing.T) {
|
||||||
|
az := icons.NerdFont.Azure
|
||||||
|
for _, c := range []struct {
|
||||||
|
e listing.Entry
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{listing.Entry{Provider: "azure", Starred: true, Recent: true}, "★ ◷ " + az},
|
||||||
|
{listing.Entry{Provider: "azure", Recent: true}, " ◷ " + az},
|
||||||
|
{listing.Entry{Provider: "azure", Starred: true}, "★ " + az}, // as wide as ◷
|
||||||
|
} {
|
||||||
|
if got := markOf(c.e, styles.Default(), icons.NerdFont, true, true, false); got != c.want {
|
||||||
|
t.Errorf("markOf(starred %v, recent %v) = %q, want %q", c.e.Starred, c.e.Recent, got, c.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestListErrors(t *testing.T) {
|
func TestListErrors(t *testing.T) {
|
||||||
e := listing.Entry{Provider: "azure", Path: []string{"platform-prod", "kv-restricted"}, Error: "403 Forbidden"}
|
e := listing.Entry{Provider: "azure", Path: []string{"platform-prod", "kv-restricted"}, Error: "403 Forbidden"}
|
||||||
if got := nameOf(e, styles.Default(), false); got != "platform-prod / kv-restricted" {
|
if got := nameOf(e, styles.Default(), false); got != "platform-prod / kv-restricted" {
|
||||||
|
|||||||
@@ -298,6 +298,8 @@ Flags:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
recentIcon := cfg.UI.RecentIcon != nil && *cfg.UI.RecentIcon
|
||||||
|
|
||||||
// Stars, notes and the layout. Demo mode keeps them in memory, so
|
// Stars, notes and the layout. Demo mode keeps them in memory, so
|
||||||
// sample vaults never end up in the real file.
|
// sample vaults never end up in the real file.
|
||||||
stateDir, err := config.StateDir()
|
stateDir, err := config.StateDir()
|
||||||
@@ -387,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, listArgs, os.Stdout)
|
return listCmd(providers, st, cachePath, timeoutFor, theme, *icons, recentIcon, 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
|
||||||
@@ -478,6 +480,7 @@ Flags:
|
|||||||
ShareRules: shareRules(cfg),
|
ShareRules: shareRules(cfg),
|
||||||
Open: open,
|
Open: open,
|
||||||
OpenAction: openAction,
|
OpenAction: openAction,
|
||||||
|
RecentIcon: recentIcon,
|
||||||
Styles: &theme,
|
Styles: &theme,
|
||||||
DefaultFg: defaultFg,
|
DefaultFg: defaultFg,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ it isn't there yet); changes apply when lazysecrets starts again.
|
|||||||
```yaml
|
```yaml
|
||||||
ui:
|
ui:
|
||||||
icons: true # Nerd Font icons (v3+); also --icons=false or LAZYSECRETS_ICONS=0
|
icons: true # Nerd Font icons (v3+); also --icons=false or LAZYSECRETS_ICONS=0
|
||||||
|
recentIcon: false # mark items used lately (Recent, R) with ◷, here and in lazysecrets list
|
||||||
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
|
||||||
|
|||||||
@@ -44,6 +44,9 @@ type Config struct {
|
|||||||
|
|
||||||
type UI struct {
|
type UI struct {
|
||||||
Icons *bool `yaml:"icons"`
|
Icons *bool `yaml:"icons"`
|
||||||
|
// RecentIcon marks items used lately (Recent, R) with ◷, in the lists
|
||||||
|
// and in lazysecrets list. Defaults to false.
|
||||||
|
RecentIcon *bool `yaml:"recentIcon"`
|
||||||
// SourcesView is "grouped" (default) or "flat"; ` + "`" + ` toggles it.
|
// SourcesView is "grouped" (default) or "flat"; ` + "`" + ` toggles it.
|
||||||
SourcesView string `yaml:"sourcesView"`
|
SourcesView string `yaml:"sourcesView"`
|
||||||
// CollapseGroups starts the Sources pane with its groups folded;
|
// CollapseGroups starts the Sources pane with its groups folded;
|
||||||
|
|||||||
@@ -131,6 +131,9 @@ type Options struct {
|
|||||||
HandlerSetup *HandlerSetup
|
HandlerSetup *HandlerSetup
|
||||||
// NoHistory stops counting uses of items for the Recent list.
|
// NoHistory stops counting uses of items for the Recent list.
|
||||||
NoHistory bool
|
NoHistory bool
|
||||||
|
// RecentIcon marks items used lately with ◷ in the lists
|
||||||
|
// (ui.recentIcon).
|
||||||
|
RecentIcon bool
|
||||||
// Open is a reference to open at startup (lazysecrets open <ref>).
|
// Open is a reference to open at startup (lazysecrets open <ref>).
|
||||||
Open *ref.Ref
|
Open *ref.Ref
|
||||||
// OpenAction, if set, is what to do with Open, rather than asking:
|
// OpenAction, if set, is what to do with Open, rather than asking:
|
||||||
@@ -287,6 +290,7 @@ type Model struct {
|
|||||||
prompt *modal // popup that has the keyboard
|
prompt *modal // popup that has the keyboard
|
||||||
linked []provider.Container // vaults opened from links, this session
|
linked []provider.Container // vaults opened from links, this session
|
||||||
open *ref.Ref // reference to open at startup
|
open *ref.Ref // reference to open at startup
|
||||||
|
recentIcon bool // mark items used lately (◷)
|
||||||
openAction string // and what to do with it, or ask
|
openAction string // and what to do with it, or ask
|
||||||
|
|
||||||
shareKey string // the config's sharing.key
|
shareKey string // the config's sharing.key
|
||||||
@@ -420,7 +424,8 @@ func New(providers []provider.Provider, opts Options) Model {
|
|||||||
if m.store == nil {
|
if m.store == nil {
|
||||||
m.store, _ = store.Load("")
|
m.store, _ = store.Load("")
|
||||||
}
|
}
|
||||||
m.sources = newSourcesList(iconSet, m.store)
|
m.recentIcon = opts.RecentIcon
|
||||||
|
m.sources = newSourcesList(iconSet, m.store, m.recentIcon)
|
||||||
m.sources.Empty = "no sources found"
|
m.sources.Empty = "no sources found"
|
||||||
m.collapsed = map[string]bool{}
|
m.collapsed = map[string]bool{}
|
||||||
m.flatSources = opts.FlatSources
|
m.flatSources = opts.FlatSources
|
||||||
|
|||||||
@@ -19,6 +19,22 @@ import (
|
|||||||
|
|
||||||
const star = "★ "
|
const star = "★ "
|
||||||
|
|
||||||
|
// recentMark marks items used lately (Recent), with ui.recentIcon.
|
||||||
|
const recentMark = "◷ "
|
||||||
|
|
||||||
|
// marks is what goes before a list row's name: ★ when it's starred for
|
||||||
|
// account and, with recent, ◷ when it was used lately.
|
||||||
|
func marks(st styles.Styles, n store.Node, account string, recent bool) string {
|
||||||
|
var s string
|
||||||
|
if n.StarredFor(account) {
|
||||||
|
s = st.Favourite.Render(star)
|
||||||
|
}
|
||||||
|
if recent && !n.LastUsed.IsZero() {
|
||||||
|
s += st.Muted.Render(recentMark)
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
func sourceKey(s provider.Source) string {
|
func sourceKey(s provider.Source) string {
|
||||||
if r, err := ref.ForSource(s); err == nil {
|
if r, err := ref.ForSource(s); err == nil {
|
||||||
return r.String()
|
return r.String()
|
||||||
@@ -94,9 +110,7 @@ func (m Model) containerRender(withSource bool) func(styles.Styles, provider.Con
|
|||||||
}
|
}
|
||||||
line += sty.Muted.Render(" " + c.Detail)
|
line += sty.Muted.Render(" " + c.Detail)
|
||||||
}
|
}
|
||||||
if n.StarredFor(c.Source.Provider) {
|
line = marks(sty, n, c.Source.Provider, m.recentIcon) + line
|
||||||
line = sty.Favourite.Render(star) + line
|
|
||||||
}
|
|
||||||
if withSource {
|
if withSource {
|
||||||
src := c.Source.Name
|
src := c.Source.Name
|
||||||
if account, ok := strings.CutPrefix(c.Source.ProviderLabel, string(c.Source.Kind)+" · "); ok {
|
if account, ok := strings.CutPrefix(c.Source.ProviderLabel, string(c.Source.Kind)+" · "); ok {
|
||||||
@@ -116,9 +130,7 @@ func (m Model) objectRender() func(styles.Styles, provider.Object, int) string {
|
|||||||
if o.Enabled != nil && !*o.Enabled {
|
if o.Enabled != nil && !*o.Enabled {
|
||||||
line = sty.Muted.Render(o.Name + " (disabled)")
|
line = sty.Muted.Render(o.Name + " (disabled)")
|
||||||
}
|
}
|
||||||
if n.StarredFor(o.Container.Source.Provider) {
|
line = marks(sty, n, o.Container.Source.Provider, m.recentIcon) + line
|
||||||
line = sty.Favourite.Render(star) + line
|
|
||||||
}
|
|
||||||
switch state, left := m.expiry(o); state {
|
switch state, left := m.expiry(o); state {
|
||||||
case expired:
|
case expired:
|
||||||
line += " " + sty.Error.Render("✗ expired")
|
line += " " + sty.Error.Render("✗ expired")
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
tea "charm.land/bubbletea/v2"
|
tea "charm.land/bubbletea/v2"
|
||||||
"charm.land/lipgloss/v2"
|
"charm.land/lipgloss/v2"
|
||||||
@@ -434,3 +435,20 @@ func TestStarIsPerAccount(t *testing.T) {
|
|||||||
t.Errorf("favourites should list alice's star only: %q", labels)
|
t.Errorf("favourites should list alice's star only: %q", labels)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRecentIcon(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
mem, _ := store.Load("")
|
||||||
|
_ = mem.Use("lazysecrets://azure/"+kv+"/secrets/api-key", time.Now(), func(*store.Node) {})
|
||||||
|
d := newDriverOpts(t, Options{Store: mem, RecentIcon: true})
|
||||||
|
d.keys("3")
|
||||||
|
scr := d.screen()
|
||||||
|
if !strings.Contains(scr, "◷ api-key") || strings.Contains(scr, "◷ db-password") {
|
||||||
|
t.Errorf("want ◷ on api-key only:\n%s", scr)
|
||||||
|
}
|
||||||
|
off := newDriverOpts(t, Options{Store: mem})
|
||||||
|
off.keys("3")
|
||||||
|
if strings.Contains(off.screen(), "◷") {
|
||||||
|
t.Errorf("◷ without ui.recentIcon:\n%s", off.screen())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -57,7 +57,7 @@ func sourceRowID(r sourceRow) string {
|
|||||||
return sourceRowKey(r.provider, r.source.ID)
|
return sourceRowKey(r.provider, r.source.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func newSourcesList(iconSet icons.Set, mem *store.Store) panes.List[sourceRow] {
|
func newSourcesList(iconSet icons.Set, mem *store.Store, recent bool) panes.List[sourceRow] {
|
||||||
return panes.NewList("[1] Sources", panes.Item[sourceRow]{
|
return panes.NewList("[1] Sources", panes.Item[sourceRow]{
|
||||||
ID: sourceRowID,
|
ID: sourceRowID,
|
||||||
// Headers have no filter text, so filtering shows just sources.
|
// Headers have no filter text, so filtering shows just sources.
|
||||||
@@ -79,8 +79,8 @@ func newSourcesList(iconSet icons.Set, mem *store.Store) panes.List[sourceRow] {
|
|||||||
node := mem.Node(sourceKey(r.source))
|
node := mem.Node(sourceKey(r.source))
|
||||||
kind := st.Kind(r.group)
|
kind := st.Kind(r.group)
|
||||||
starred := ""
|
starred := ""
|
||||||
if r.kind == rowSource && node.StarredFor(r.source.Provider) {
|
if r.kind == rowSource {
|
||||||
starred = st.Favourite.Render(star)
|
starred = marks(st, node, r.source.Provider, recent)
|
||||||
}
|
}
|
||||||
switch {
|
switch {
|
||||||
case r.kind == rowLinks:
|
case r.kind == rowLinks:
|
||||||
|
|||||||
Reference in new issue
Block a user