feat: l for details; details for vaults, scopes and files; getting started; the age of cached lists
#17
No files matched your search
@@ -23,7 +23,8 @@ A terminal UI for finding, reading and sharing secrets. Inspired by the fantasti
|
|||||||
- **Favourites**: star the secrets you use most and get back to them in one
|
- **Favourites**: star the secrets you use most and get back to them in one
|
||||||
key.
|
key.
|
||||||
- **Expiry warnings**: secrets, keys and certificates that expire soon are
|
- **Expiry warnings**: secrets, keys and certificates that expire soon are
|
||||||
marked.
|
marked, listed together in the status pane, and `lazysecrets expiring`
|
||||||
|
checks every vault from a pipeline.
|
||||||
- **Your way**: rebind any key, pick a theme, and add custom commands to run
|
- **Your way**: rebind any key, pick a theme, and add custom commands to run
|
||||||
on a secret.
|
on a secret.
|
||||||
- **Try it first**: `lazysecrets --demo` runs on built-in sample data.
|
- **Try it first**: `lazysecrets --demo` runs on built-in sample data.
|
||||||
|
|||||||
@@ -0,0 +1,163 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"text/tabwriter"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.kebcloud.no/keb/lazysecrets/internal/provider"
|
||||||
|
"git.kebcloud.no/keb/lazysecrets/internal/ref"
|
||||||
|
)
|
||||||
|
|
||||||
|
// lazysecrets expiring lists what has expired, or will within --days,
|
||||||
|
// in every vault of every account: for a pipeline or a cron job. Expiry
|
||||||
|
// dates come with the lists, so it's one request per vault and type,
|
||||||
|
// a few at a time. Only Key Vault has expiry dates.
|
||||||
|
|
||||||
|
// errExpiring makes the exit status 1 when anything was listed.
|
||||||
|
var errExpiring = errors.New("expiring")
|
||||||
|
|
||||||
|
// listLimit is how many lists load at once.
|
||||||
|
const listLimit = 8
|
||||||
|
|
||||||
|
type expiringOpts struct {
|
||||||
|
days int
|
||||||
|
timeout time.Duration
|
||||||
|
now time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseExpiring(args []string, days int) (int, error) {
|
||||||
|
fs := flag.NewFlagSet("expiring", flag.ContinueOnError)
|
||||||
|
n := fs.Int("days", days, "list what expires within this many days (default: expiryWarningDays, else 30)")
|
||||||
|
if err := fs.Parse(args); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if fs.NArg() > 0 || *n < 0 {
|
||||||
|
return 0, fmt.Errorf("usage: lazysecrets expiring [--days N]")
|
||||||
|
}
|
||||||
|
return *n, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func expiring(providers []provider.Provider, o expiringOpts, out, errs io.Writer) error {
|
||||||
|
var (
|
||||||
|
mu sync.Mutex
|
||||||
|
found []provider.Object
|
||||||
|
seen = map[string]bool{}
|
||||||
|
failed int
|
||||||
|
vaults int
|
||||||
|
wg sync.WaitGroup
|
||||||
|
limit = make(chan struct{}, listLimit)
|
||||||
|
within = o.now.AddDate(0, 0, o.days)
|
||||||
|
warnErr = func(what string, err error) {
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
failed++
|
||||||
|
fmt.Fprintf(errs, "lazysecrets: %s: %v\n", what, err)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
run := func(f func(ctx context.Context)) {
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
limit <- struct{}{}
|
||||||
|
defer func() { <-limit }()
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), o.timeout)
|
||||||
|
defer cancel()
|
||||||
|
f(ctx)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
var vaultsDone sync.WaitGroup
|
||||||
|
for _, p := range providers {
|
||||||
|
if p.Kind() != provider.KindAzure {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
vaultsDone.Add(1)
|
||||||
|
run(func(ctx context.Context) {
|
||||||
|
defer vaultsDone.Done()
|
||||||
|
srcs, err := p.Sources(ctx)
|
||||||
|
if err != nil {
|
||||||
|
warnErr(p.Label(), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, src := range srcs {
|
||||||
|
vaultsDone.Add(1)
|
||||||
|
run(func(ctx context.Context) {
|
||||||
|
defer vaultsDone.Done()
|
||||||
|
cs, err := p.Containers(ctx, src)
|
||||||
|
if err != nil {
|
||||||
|
warnErr(src.Name, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, c := range cs {
|
||||||
|
mu.Lock()
|
||||||
|
fresh := !seen[c.ID]
|
||||||
|
seen[c.ID] = true
|
||||||
|
if fresh {
|
||||||
|
vaults++
|
||||||
|
}
|
||||||
|
mu.Unlock()
|
||||||
|
if !fresh { // the same vault through another account
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, t := range c.ObjectTypes() {
|
||||||
|
run(func(ctx context.Context) {
|
||||||
|
objs, err := p.Objects(ctx, c, t)
|
||||||
|
if err != nil {
|
||||||
|
warnErr(c.Name+" "+strings.ToLower(string(t))+"s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
for _, o := range objs {
|
||||||
|
if !o.Expires.IsZero() && o.Expires.Before(within) {
|
||||||
|
found = append(found, o)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// Listing vaults queues the lists of their secrets, so wait for both.
|
||||||
|
vaultsDone.Wait()
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
slices.SortFunc(found, func(a, b provider.Object) int { return a.Expires.Compare(b.Expires) })
|
||||||
|
if len(found) == 0 {
|
||||||
|
fmt.Fprintf(errs, "nothing expires within %d days (%d vaults)\n", o.days, vaults)
|
||||||
|
if failed > 0 {
|
||||||
|
return fmt.Errorf("%d lists couldn't be read", failed)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
tw := tabwriter.NewWriter(out, 0, 0, 2, ' ', 0)
|
||||||
|
fmt.Fprintln(tw, "LEFT\tEXPIRES\tTYPE\tVAULT\tNAME\tLINK")
|
||||||
|
for _, x := range found {
|
||||||
|
left := "expired"
|
||||||
|
if d := x.Expires.Sub(o.now); d > 0 {
|
||||||
|
left = fmt.Sprintf("%dd", int(d.Hours()/24))
|
||||||
|
}
|
||||||
|
link := ""
|
||||||
|
if r, err := ref.ForObject(x, ""); err == nil {
|
||||||
|
link = r.String()
|
||||||
|
}
|
||||||
|
fmt.Fprintf(tw, "%s\t%s\t%s\t%s\t%s\t%s\n", left, x.Expires.Local().Format("2006-01-02"),
|
||||||
|
strings.ToLower(string(x.Type)), x.Container.Name, x.Name, link)
|
||||||
|
}
|
||||||
|
if err := tw.Flush(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if failed > 0 {
|
||||||
|
fmt.Fprintf(errs, "lazysecrets: %d lists couldn't be read: the list may be incomplete\n", failed)
|
||||||
|
}
|
||||||
|
return errExpiring
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.kebcloud.no/keb/lazysecrets/internal/provider"
|
||||||
|
"git.kebcloud.no/keb/lazysecrets/internal/provider/fake"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestExpiring(t *testing.T) {
|
||||||
|
providers := []provider.Provider{fake.Azure(0), fake.Databricks(0)}
|
||||||
|
now := time.Date(2026, 10, 1, 0, 0, 0, 0, time.UTC)
|
||||||
|
var out, errs bytes.Buffer
|
||||||
|
err := expiring(providers, expiringOpts{days: 30, timeout: time.Minute, now: now}, &out, &errs)
|
||||||
|
if !errors.Is(err, errExpiring) {
|
||||||
|
t.Fatalf("err %v (want errExpiring: something listed)", err)
|
||||||
|
}
|
||||||
|
lines := strings.Split(strings.TrimSpace(out.String()), "\n")
|
||||||
|
if len(lines) != 2 || !strings.HasPrefix(lines[1], "expired 2026-03-01 certificate kv-app-prod old-tls") ||
|
||||||
|
!strings.Contains(lines[1], "lazysecrets://azure/kv-app-prod.vault.azure.net/certificates/old-tls") {
|
||||||
|
t.Errorf("out:\n%s", out.String())
|
||||||
|
}
|
||||||
|
if !strings.Contains(errs.String(), "kv-restricted") {
|
||||||
|
t.Errorf("unreadable vaults not said:\n%s", errs.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Further ahead, the soonest first.
|
||||||
|
out.Reset()
|
||||||
|
expiring(providers, expiringOpts{days: 400, timeout: time.Minute, now: now}, &out, &errs)
|
||||||
|
if got := strings.Count(out.String(), "\n"); got != 4 || !strings.Contains(out.String(), "app-tls") {
|
||||||
|
t.Errorf("400 days:\n%s", out.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := parseExpiring([]string{"--days", "-1"}, 30); err == nil {
|
||||||
|
t.Error("negative days accepted")
|
||||||
|
}
|
||||||
|
}
|
||||||
+18
-1
@@ -4,6 +4,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@@ -33,7 +34,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
if err := run(); err != nil {
|
if err := run(); errors.Is(err, errExpiring) {
|
||||||
|
os.Exit(1) // listed: a pipeline fails on it
|
||||||
|
} else if err != nil {
|
||||||
fmt.Fprintln(os.Stderr, "lazysecrets:", err)
|
fmt.Fprintln(os.Stderr, "lazysecrets:", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
@@ -118,6 +121,9 @@ func run() error {
|
|||||||
@Microsoft.KeyVault(...) or {{secrets/scope/key}}
|
@Microsoft.KeyVault(...) or {{secrets/scope/key}}
|
||||||
lazysecrets [flags] get <ref> print the value a lazysecrets:// link points at,
|
lazysecrets [flags] get <ref> print the value a lazysecrets:// link points at,
|
||||||
for scripts: VAR=$(lazysecrets get <ref>)
|
for scripts: VAR=$(lazysecrets get <ref>)
|
||||||
|
lazysecrets [flags] expiring [--days N]
|
||||||
|
list what has expired or will within N days
|
||||||
|
(default 30) in every vault; exits 1 if any
|
||||||
lazysecrets [flags] keybindings print every action and its keys (with your
|
lazysecrets [flags] keybindings print every action and its keys (with your
|
||||||
config applied), as config to copy and edit
|
config applied), as config to copy and edit
|
||||||
lazysecrets handler install [--terminal NAME]
|
lazysecrets handler install [--terminal NAME]
|
||||||
@@ -140,6 +146,7 @@ Flags:
|
|||||||
}
|
}
|
||||||
|
|
||||||
var open, getRef *ref.Ref
|
var open, getRef *ref.Ref
|
||||||
|
var expiringArgs []string
|
||||||
printKeys := false
|
printKeys := false
|
||||||
switch args := flag.Args(); {
|
switch args := flag.Args(); {
|
||||||
case len(args) == 1 && args[0] == "keybindings":
|
case len(args) == 1 && args[0] == "keybindings":
|
||||||
@@ -150,6 +157,8 @@ Flags:
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
getRef = &r
|
getRef = &r
|
||||||
|
case len(args) > 0 && args[0] == "expiring":
|
||||||
|
expiringArgs = append([]string{}, args[1:]...)
|
||||||
case len(args) > 0 && args[0] == "handler":
|
case len(args) > 0 && args[0] == "handler":
|
||||||
return handler(*configPath, args[1:])
|
return handler(*configPath, args[1:])
|
||||||
case len(args) > 0 && args[0] == "launch":
|
case len(args) > 0 && args[0] == "launch":
|
||||||
@@ -298,6 +307,14 @@ Flags:
|
|||||||
// the standard logger, which would draw over the TUI.
|
// the standard logger, which would draw over the TUI.
|
||||||
log.SetOutput(io.Discard)
|
log.SetOutput(io.Discard)
|
||||||
|
|
||||||
|
if expiringArgs != nil {
|
||||||
|
days, err := parseExpiring(expiringArgs, expiryDays)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return expiring(providers, expiringOpts{days: days, timeout: *timeout, now: time.Now()}, os.Stdout, os.Stderr)
|
||||||
|
}
|
||||||
|
|
||||||
if getRef != nil {
|
if getRef != nil {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), *timeout)
|
ctx, cancel := context.WithTimeout(context.Background(), *timeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|||||||
@@ -64,3 +64,23 @@ Secrets, keys and certificates, each on their own tab of the Secrets pane
|
|||||||
key (PEM public key) or certificate; private key material is never
|
key (PEM public key) or certificate; private key material is never
|
||||||
fetched. `v` lists an item's versions. Items expiring within
|
fetched. `v` lists an item's versions. Items expiring within
|
||||||
`expiryWarningDays` (30 by default) are marked `⚠ 12d`, expired ones `✗`.
|
`expiryWarningDays` (30 by default) are marked `⚠ 12d`, expired ones `✗`.
|
||||||
|
|
||||||
|
The status pane (`0`) lists them across every vault loaded, opened or
|
||||||
|
cached, soonest first, and its line counts them (`⚠ 3 expiring`); `enter`
|
||||||
|
on one goes to it. That makes no requests of its own: expiry dates come
|
||||||
|
with the lists. It says how many vaults it covers; `ctrl+a` in search
|
||||||
|
loads the rest.
|
||||||
|
|
||||||
|
For a pipeline or a cron job, `lazysecrets expiring` lists them in every
|
||||||
|
vault of every account, loading each vault's lists (a few at a time), and
|
||||||
|
exits 1 when it lists anything:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ lazysecrets expiring --days 30
|
||||||
|
LEFT EXPIRES TYPE VAULT NAME LINK
|
||||||
|
expired 2026-03-01 certificate kv-app-prod old-tls lazysecrets://azure/kv-app-prod.vault.azure.net/certificates/old-tls
|
||||||
|
12d 2026-10-23 secret kv-app-prod api-key lazysecrets://azure/kv-app-prod.vault.azure.net/secrets/api-key
|
||||||
|
```
|
||||||
|
|
||||||
|
`--days` defaults to `expiryWarningDays`. Lists it can't read are named on
|
||||||
|
stderr.
|
||||||
@@ -841,6 +841,12 @@ func (m *Model) handleKey(msg tea.KeyPressMsg) (cmd tea.Cmd, quit bool) {
|
|||||||
m.focus = paneDetail // its rows are on the right
|
m.focus = paneDetail // its rows are on the right
|
||||||
case m.focus == paneSources && (key.Matches(msg, k.Enter) || key.Matches(msg, k.Reveal)) && m.toggleGroup():
|
case m.focus == paneSources && (key.Matches(msg, k.Enter) || key.Matches(msg, k.Reveal)) && m.toggleGroup():
|
||||||
// folded or unfolded a provider group
|
// folded or unfolded a provider group
|
||||||
|
case m.focus == paneDetail && key.Matches(msg, k.Enter) && m.fieldLink() != "":
|
||||||
|
r, err := ref.Parse(m.fieldLink())
|
||||||
|
if err == nil {
|
||||||
|
f, _ := m.fields.Selected()
|
||||||
|
m.startNav(r, actGo, f.Row)
|
||||||
|
}
|
||||||
case key.Matches(msg, k.Enter):
|
case key.Matches(msg, k.Enter):
|
||||||
if m.focus < paneDetail {
|
if m.focus < paneDetail {
|
||||||
m.focus++
|
m.focus++
|
||||||
@@ -1738,6 +1744,14 @@ func (m Model) problemText(providerID, err string) string {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// fieldLink is the link of the selected Fields row, if it has one.
|
||||||
|
func (m Model) fieldLink() string {
|
||||||
|
if f, ok := m.fields.Selected(); ok && m.fields.Shown() {
|
||||||
|
return f.Link
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
// loginFor is the sign-in L runs: the selected account's, if it needs
|
// loginFor is the sign-in L runs: the selected account's, if it needs
|
||||||
// one, else the last one asked for.
|
// one, else the last one asked for.
|
||||||
func (m Model) loginFor() *provider.AuthError {
|
func (m Model) loginFor() *provider.AuthError {
|
||||||
|
|||||||
@@ -1236,3 +1236,25 @@ func TestStatusPane(t *testing.T) {
|
|||||||
t.Errorf("back to %v", d.m.focus)
|
t.Errorf("back to %v", d.m.focus)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestExpiringInStatus(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
d := newDriverOpts(t, Options{ExpiryWarning: 30 * 24 * time.Hour}, fake.Azure(0))
|
||||||
|
d.keys("3", "]", "]") // load kv-app-prod's certificates
|
||||||
|
if !strings.Contains(d.screen(), "⚠ 1 expiring") {
|
||||||
|
t.Errorf("status line:\n%s", d.screen())
|
||||||
|
}
|
||||||
|
d.keys("0")
|
||||||
|
scr := d.screen()
|
||||||
|
for _, want := range []string{"expiring ✗ expired old-tls · kv-app-prod", "in 1 of 3 vaults (ctrl+a in search loads all)"} {
|
||||||
|
if !strings.Contains(scr, want) {
|
||||||
|
t.Errorf("missing %q:\n%s", want, scr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
d.keys("3", "[", "[", "0", "enter") // elsewhere, then to it from the status pane
|
||||||
|
d.m.fields.SelectWhere(func(f panes.Field) bool { return f.Label == "expiring" })
|
||||||
|
d.keys("enter")
|
||||||
|
if _, v, o := d.selected(); v != "kv-app-prod" || o != "old-tls" {
|
||||||
|
t.Errorf("enter went to %s/%s\n%s", v, o, d.screen())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"cmp"
|
||||||
|
"fmt"
|
||||||
|
"slices"
|
||||||
|
|
||||||
|
"git.kebcloud.no/keb/lazysecrets/internal/provider"
|
||||||
|
"git.kebcloud.no/keb/lazysecrets/internal/ref"
|
||||||
|
"git.kebcloud.no/keb/lazysecrets/internal/ui/panes"
|
||||||
|
)
|
||||||
|
|
||||||
|
// The status pane lists what has expired or will soon, across every vault
|
||||||
|
// loaded (opened, searched, or from the cache): expiry dates come with
|
||||||
|
// the lists, so this makes no requests. enter on one goes to it.
|
||||||
|
|
||||||
|
type expiringItem struct {
|
||||||
|
o provider.Object
|
||||||
|
row string // its source's row key, to go to it
|
||||||
|
}
|
||||||
|
|
||||||
|
// expiringItems are the loaded objects that have expired or will within
|
||||||
|
// the warning period, soonest first.
|
||||||
|
func (m Model) expiringItems() []expiringItem {
|
||||||
|
var out []expiringItem
|
||||||
|
for _, p := range m.providers {
|
||||||
|
for _, src := range m.sourceResults[p.ID()].sources {
|
||||||
|
row := sourceRowKey(p.ID(), src.ID)
|
||||||
|
for _, c := range m.cache.containers[row].value {
|
||||||
|
for _, t := range c.ObjectTypes() {
|
||||||
|
for _, o := range m.cache.objects[containerKey(c)+"|"+string(t)].value {
|
||||||
|
if state, _ := m.expiry(o); state != notExpiring {
|
||||||
|
out = append(out, expiringItem{o, row})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
slices.SortFunc(out, func(a, b expiringItem) int {
|
||||||
|
return cmp.Or(a.o.Expires.Compare(b.o.Expires), cmp.Compare(a.o.Name, b.o.Name))
|
||||||
|
})
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// expiringFields are the status pane's rows for them, and how much of
|
||||||
|
// everything they cover.
|
||||||
|
func (m Model) expiringFields() []panes.Field {
|
||||||
|
items := m.expiringItems()
|
||||||
|
var out []panes.Field
|
||||||
|
for _, it := range items {
|
||||||
|
state, left := m.expiry(it.o)
|
||||||
|
mark := "⚠ " + daysLeft(left)
|
||||||
|
if state == expired {
|
||||||
|
mark = "✗ expired"
|
||||||
|
}
|
||||||
|
f := panes.Field{Value: mark + " " + it.o.Name + " · " + it.o.Container.Name, Err: state == expired, Row: it.row}
|
||||||
|
if r, err := ref.ForObject(it.o, ""); err == nil {
|
||||||
|
f.Link = r.String()
|
||||||
|
}
|
||||||
|
out = append(out, f)
|
||||||
|
}
|
||||||
|
cov := m.coverageOf(provider.KindAzure)
|
||||||
|
switch {
|
||||||
|
case cov.vaults == 0 && len(items) == 0:
|
||||||
|
return nil
|
||||||
|
case !cov.complete():
|
||||||
|
out = append(out, panes.Field{Value: fmt.Sprintf("in %d of %d vaults", cov.vaultsLoaded, cov.vaults) +
|
||||||
|
withKey(m.keys.SearchLoadAll, " (%s in search loads all)"), Info: true})
|
||||||
|
case len(items) == 0:
|
||||||
|
out = append(out, panes.Field{Value: fmt.Sprintf("nothing within %d days", int(m.expiryWarning.Hours()/24)), Info: true})
|
||||||
|
}
|
||||||
|
out[0].Label = "expiring"
|
||||||
|
return out
|
||||||
|
}
|
||||||
@@ -38,6 +38,9 @@ func (m Model) statusLine() string {
|
|||||||
failing++
|
failing++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if n := len(m.expiringItems()); n > 0 {
|
||||||
|
parts = append(parts, m.st.Warning.Render(fmt.Sprintf("⚠ %d expiring", n)))
|
||||||
|
}
|
||||||
if accounts > 0 {
|
if accounts > 0 {
|
||||||
s := plural(accounts, "account")
|
s := plural(accounts, "account")
|
||||||
if failing > 0 {
|
if failing > 0 {
|
||||||
@@ -88,6 +91,8 @@ func (m Model) statusFields() []panes.Field {
|
|||||||
add(f)
|
add(f)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
out = append(out, m.expiringFields()...)
|
||||||
|
|
||||||
for i, path := range m.configFiles {
|
for i, path := range m.configFiles {
|
||||||
label := ""
|
label := ""
|
||||||
if i == 0 {
|
if i == 0 {
|
||||||
|
|||||||
@@ -20,12 +20,15 @@ type Fields struct {
|
|||||||
|
|
||||||
// Field is a row: a value under a label. Info rows (loading, errors)
|
// Field is a row: a value under a label. Info rows (loading, errors)
|
||||||
// can't be copied; Err ones are errors. A row can also be opened: URL in
|
// can't be copied; Err ones are errors. A row can also be opened: URL in
|
||||||
// the browser (o), Path in the editor (e), or Provider signed in to (L).
|
// the browser (o), Path in the editor (e), Provider signed in to (L), or
|
||||||
|
// Link gone to (enter).
|
||||||
type Field struct {
|
type Field struct {
|
||||||
Label, Value string
|
Label, Value string
|
||||||
Info, Err bool
|
Info, Err bool
|
||||||
|
|
||||||
URL, Path, Provider string
|
URL, Path, Provider string
|
||||||
|
// Link is a lazysecrets link enter goes to, in the source of row Row.
|
||||||
|
Link, Row string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewFields() Fields {
|
func NewFields() Fields {
|
||||||
|
|||||||
Reference in new issue
Block a user