feat: lazysecrets list prints everything lazysecrets can find, for fzf and editor pickers (#70)

`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>
This commit was merged in pull request #70.
This commit is contained in:
keb authored and Karl-Einar Bergstrøm committed 2026-10-14 21:30:44 +02:00
1 parent dbb0eb2bbd
commit 39454d47a8
45 files changed
+2107 -222

No files matched your search

+38 -13
View File
@@ -131,8 +131,15 @@ type Options struct {
HandlerSetup *HandlerSetup
// NoHistory stops counting uses of items for the Recent list.
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 *ref.Ref
// OpenAction, if set, is what to do with Open, rather than asking:
// go to it, and maybe copy or show its value (OpenActions). It ends in
// the list, like Go to, rather than on the details.
OpenAction string
// Styles, if set, replaces the default theme (styles.FromTheme), and
// DefaultFg the terminal's default text colour.
Styles *styles.Styles
@@ -206,9 +213,12 @@ type Model struct {
statusW int // the status pane's width: the left column's, zoomed or not
changelog []panes.Field
cacheDirty bool
sourcesAt map[string]time.Time
placeDir string
savedPlace string // the link last saved as the place
// cacheDropped are cache keys deleted since the last save, so saving
// deletes them from the file too.
cacheDropped map[string]bool
sourcesAt map[string]time.Time
placeDir string
savedPlace string // the link last saved as the place
// A pending clear of a copied value, counted down in the footer.
// clipToken identifies the latest copy so older ticks are ignored.
@@ -283,6 +293,8 @@ type Model struct {
prompt *modal // popup that has the keyboard
linked []provider.Container // vaults opened from links, this session
open *ref.Ref // reference to open at startup
recentIcon bool // mark items used lately (◷)
openAction string // and what to do with it, or ask
shareKey string // the config's sharing.key
shareRules share.Rules // and sharing.keyRules
@@ -415,7 +427,8 @@ func New(providers []provider.Provider, opts Options) Model {
if m.store == nil {
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.collapsed = map[string]bool{}
m.flatSources = opts.FlatSources
@@ -447,7 +460,7 @@ func New(providers []provider.Provider, opts Options) Model {
}
m.defaultFg = opts.DefaultFg
m.help = newHelp(m.st)
m.open = opts.Open
m.open, m.openAction = opts.Open, opts.OpenAction
saved := m.store.Layout()
m.split, m.heights = saved.Split, saved.Heights
@@ -521,18 +534,26 @@ func (m Model) Init() tea.Cmd {
cmds = append(cmds, m.fetchLatest(false))
}
if m.open != nil {
r := *m.open
cmds = append(cmds, func() tea.Msg { return openRefMsg{r} })
msg := openRefMsg{ref: *m.open, action: m.openAction}
cmds = append(cmds, func() tea.Msg { return msg })
}
return tea.Batch(cmds...)
}
// openRefMsg starts opening a reference from outside the key handler.
type openRefMsg struct{ ref ref.Ref }
// openRefMsg starts opening a reference from outside the key handler:
// asking what to do with it, or doing action (OpenActions).
type openRefMsg struct {
ref ref.Ref
action string
}
// OpenActions are what lazysecrets open --action takes, the Open link
// menu's choices that go there.
var OpenActions = map[string]linkAction{"go": actGo, "copy": actCopy, "show": actShow}
// OpenLink is a message offering r in the Open link popup, for links
// handed over by the link handler (Program.Send).
func OpenLink(r ref.Ref) tea.Msg { return openRefMsg{r} }
func OpenLink(r ref.Ref) tea.Msg { return openRefMsg{ref: r} }
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmds []tea.Cmd
@@ -589,7 +610,11 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case openRefMsg:
m.deferHandlerSetup() // the link first
cmds = append(cmds, m.offerRef("Open link", msg.ref, false))
if act, ok := OpenActions[msg.action]; ok && !msg.ref.IsShare() {
cmds = append(cmds, m.findRef(msg.ref, act))
} else {
cmds = append(cmds, m.offerRef("Open link", msg.ref, false))
}
case resolvedMsg:
m.handleResolved(msg)
@@ -666,7 +691,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if src, ok := m.selectedSource(); ok {
m.logErrOf(src.Name, msg.err)
}
delete(m.cache.containers, msg.key)
uncache(&m, m.cache.containers, msg.key)
m.containers.SetError(msg.err)
} else {
m.cache.containers[msg.key] = entry[[]provider.Container]{msg.items, m.now()}
@@ -687,7 +712,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if c, ok := m.containers.Selected(); ok {
m.logErrOf(c.Name, msg.err)
}
delete(m.cache.objects, msg.key)
uncache(&m, m.cache.objects, msg.key)
m.secrets.SetError(msg.err)
} else {
if msg.bound != nil {