diff --git a/cmd/lazysecrets/list.go b/cmd/lazysecrets/list.go index 23cb504..7d9dc52 100644 --- a/cmd/lazysecrets/list.go +++ b/cmd/lazysecrets/list.go @@ -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 +} diff --git a/cmd/lazysecrets/list_test.go b/cmd/lazysecrets/list_test.go new file mode 100644 index 0000000..dd5e6e2 --- /dev/null +++ b/cmd/lazysecrets/list_test.go @@ -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) + } +} diff --git a/cmd/lazysecrets/main.go b/cmd/lazysecrets/main.go index 1ae7411..1194ad8 100644 --- a/cmd/lazysecrets/main.go +++ b/cmd/lazysecrets/main.go @@ -122,6 +122,7 @@ func run() error { lazysecrets [flags] get print the value a lazysecrets:// link points at, for scripts: VAR=$(lazysecrets get ) 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 diff --git a/docs/links.md b/docs/links.md index 5b9639b..171eb19 100644 --- a/docs/links.md +++ b/docs/links.md @@ -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"}