`lazysecrets list` prints every source, vault and secret lazysecrets can find, one per line, for fzf, editor pickers and scripts: what Go to searches, without the TUI. What's cached prints at once; what's missing or older than `--max-age` loads in the background and streams in. It stops when the reader goes (a pick in fzf), saving what it loaded. - Text: kind, mark (✗/⚠, ★, ◷ and the provider icon), name, status (why it couldn't be read, or expired / expires in 12d), note, account and link, tab-separated; kind first and account and link last, so new fields go between. Coloured from the theme; `--color never`, `--icons=false`, `--stars=false`, `--recent`, `--notes=false`, `--expiry=false`. - `--format json`: an object per line, with starred, recent, lastUsed and uses, expires, enabled, note, reference and url too. - `--kind` lists only some kinds, and doesn't load lists only other kinds need; `--cached` loads nothing. - `lazysecrets open --action go|copy|show` skips the "what to do" menu and ends in the list, like Go to; `open` and `get` take `--account`, the account list printed, rather than asking which. - The cache file moved to `internal/cachefile`: saves merge with the file under a lock, so list and the TUI don't undo each other. - `ui.recentIcon` marks items used lately with ◷ in the TUI's lists too; Recent now means the last 90 days. - --demo has stars, notes, recent use, and expired, expiring and disabled secrets. - docs/integration.md documents get, list and an fzf picker; docs/links.md is docs/sharing.md. Reviewed-on: #70 Co-authored-by: Karl-Einar Bergstrøm <karl-einarb@outlook.com>
96 lines
3.0 KiB
Go
96 lines
3.0 KiB
Go
package app
|
|
|
|
import (
|
|
"maps"
|
|
"slices"
|
|
"time"
|
|
|
|
tea "charm.land/bubbletea/v2"
|
|
|
|
"git.kebcloud.no/keb/lazysecrets/internal/provider"
|
|
)
|
|
|
|
// cacheTTL is how long cached metadata is shown without reloading. Older
|
|
// entries are still shown immediately, then reloaded in the background.
|
|
const cacheTTL = 2 * time.Minute
|
|
|
|
// cache holds metadata: never secret values or errors. It's kept between
|
|
// runs in the cache file (persist.go).
|
|
type cache struct {
|
|
containers map[string]entry[[]provider.Container] // by source key
|
|
objects map[string]entry[[]provider.Object] // by container key (ID|type)
|
|
details map[string]entry[provider.Object] // Describe results by ID@version
|
|
// cursor remembers the selected child per parent key, so returning to
|
|
// a subscription or vault puts the cursor back where it was.
|
|
cursor map[string]string
|
|
}
|
|
|
|
type entry[T any] struct {
|
|
value T
|
|
at time.Time
|
|
}
|
|
|
|
func newCache() cache {
|
|
return cache{
|
|
containers: map[string]entry[[]provider.Container]{},
|
|
objects: map[string]entry[[]provider.Object]{},
|
|
details: map[string]entry[provider.Object]{},
|
|
cursor: map[string]string{},
|
|
}
|
|
}
|
|
|
|
func (e entry[T]) fresh(now time.Time) bool { return now.Sub(e.at) < cacheTTL }
|
|
|
|
// detailKey includes the account: the result carries its container, and
|
|
// later requests for the object are routed by it.
|
|
func detailKey(o provider.Object) string {
|
|
return containerKey(o.Container) + "|" + o.ID + "@" + o.Version
|
|
}
|
|
|
|
func (c cache) rememberCursor(parent, child string) {
|
|
if parent != "" && child != "" {
|
|
c.cursor[parent] = child
|
|
}
|
|
}
|
|
|
|
// refreshShown shows the selected secret's newer metadata when its list
|
|
// reloaded with it changed from before (a cached list refreshed in the
|
|
// background), and describes it afresh: its cached details came from
|
|
// the old listing.
|
|
func (m *Model) refreshShown(before []provider.Object) tea.Cmd {
|
|
s, ok := m.secrets.Selected()
|
|
if !ok || m.secrets.SelectedID() != m.selSecret {
|
|
return nil // a new selection is shown as usual
|
|
}
|
|
i := slices.IndexFunc(before, func(o provider.Object) bool { return o.ID == s.ID })
|
|
if i < 0 || sameListing(before[i], s) {
|
|
return nil
|
|
}
|
|
m.detail.Refresh(s)
|
|
uncache(m, m.cache.details, detailKey(s))
|
|
return m.describe(s)
|
|
}
|
|
|
|
// uncache deletes key from a cache, and, at the next save, from the file.
|
|
func uncache[T any](m *Model, c map[string]entry[T], key string) {
|
|
if _, ok := c[key]; !ok {
|
|
return
|
|
}
|
|
delete(c, key)
|
|
if m.cacheDropped == nil {
|
|
m.cacheDropped = map[string]bool{}
|
|
}
|
|
m.cacheDropped[key] = true
|
|
m.cacheDirty = true
|
|
}
|
|
|
|
// sameListing reports whether a and b list the same: what a list
|
|
// says of an object, not what describing it adds.
|
|
func sameListing(a, b provider.Object) bool {
|
|
enabled := func(e *bool) bool { return e == nil || *e }
|
|
return a.Name == b.Name && a.Version == b.Version && enabled(a.Enabled) == enabled(b.Enabled) &&
|
|
a.Updated.Equal(b.Updated) && a.Expires.Equal(b.Expires) && a.ContentType == b.ContentType &&
|
|
maps.Equal(a.Tags, b.Tags) && a.Reference == b.Reference && a.URL == b.URL &&
|
|
slices.Equal(a.Attrs, b.Attrs)
|
|
}
|