Files
lazysecrets/internal/ui/app/status.go
T
2026-10-11 14:29:38 +02:00

198 lines
6.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package app
import (
"errors"
"fmt"
"strings"
"time"
"git.kebcloud.no/keb/lazysecrets/internal/provider"
"git.kebcloud.no/keb/lazysecrets/internal/ui/panes"
"git.kebcloud.no/keb/lazysecrets/internal/update"
)
// The status pane, like lazygit's: a line on top of the left panes with
// the version, a newer release and how the accounts are doing. Focused
// (0), the right pane has the rest: updates, each account, the files
// lazysecrets uses, links, and the changelog.
// logo heads the status pane's rows.
var logo = []string{
` _ _`,
`| | __ _ _____ _ ___ ___ ___ _ __ ___| |_ ___`,
`| |/ _` + "`" + ` |_ / | | / __|/ _ \/ __| '__/ _ \ __/ __|`,
`| | (_| |/ /| |_| \__ \ __/ (__| | | __/ |_\__ \`,
`|_|\__,_/___|\__, |___/\___|\___|_| \___|\__|___/`,
` |___/`,
}
// statusH is the status pane's height: one line in a box.
const statusH = 3
// statusLine is what the status pane says while it isn't focused.
func (m Model) statusLine() string {
var parts []string
switch {
case m.updated != "":
parts = append(parts, m.st.Notice.Render("→ "+m.updated+" (restart)"))
case m.newer != nil:
parts = append(parts, m.st.Notice.Render("↑ "+m.newer.Tag+withKey(m.keys.Update, " %s")))
}
accounts, failing := 0, 0
for _, p := range m.providers {
if p.Kind() == provider.KindRepo {
continue
}
accounts++
if r, ok := m.sourceResults[p.ID()]; ok && r.err != nil {
failing++
}
}
if n := len(m.expiringItems()); n > 0 {
parts = append(parts, m.st.Warning.Render(fmt.Sprintf("⚠ %d expiring", n)))
}
if accounts > 0 {
s := plural(accounts, "account")
if failing > 0 {
s += " · " + m.st.Error.Render(fmt.Sprintf("%d need sign-in", failing))
}
parts = append(parts, s)
}
return " " + strings.Join(parts, m.st.Muted.Render(" · "))
}
func (m Model) statusView() string {
title := "[0] lazysecrets"
if v := m.versionLabel(); v != "" {
title += " " + v
}
return panes.Box(m.st, panes.TitleStyle(m.st, m.active(paneStatus)).Render(title), "",
[]string{m.statusLine()}, m.statusW, statusH, m.active(paneStatus))
}
// statusFields are the status pane's rows on the right.
func (m Model) statusFields() []panes.Field {
var out []panes.Field
add := func(f panes.Field) {
if f.Value != "" {
out = append(out, f)
}
}
// A release's rows open its release page.
version := panes.Field{Label: "version", Value: cmpOr(m.versionLabel(), "dev")}
if strings.HasPrefix(version.Value, "v") {
version.URL = update.DefaultRepo + "/releases/tag/" + version.Value
}
add(version)
upd := panes.Field{Label: "update", Value: m.updateStatus(), Info: m.newer == nil}
if m.newer != nil {
upd.URL = update.DefaultRepo + "/releases/tag/" + m.newer.Tag
}
add(upd)
add(panes.Field{Label: "license", Value: "GPL-3.0-or-later · © 2026 Karl-Einar Bergstrøm", Info: true})
for i, p := range m.providers {
f := panes.Field{Value: p.Label() + " · " + m.accountStatus(p), Provider: p.ID()}
if i == 0 {
f.Label = "accounts"
}
var auth *provider.AuthError
f.Err = errors.As(m.sourceResults[p.ID()].err, &auth)
add(f)
}
out = append(out, m.expiringFields()...)
for i, path := range m.configFiles {
label := ""
if i == 0 {
label = "config"
}
add(panes.Field{Label: label, Value: path, Path: path})
}
add(panes.Field{Label: "state", Value: m.statePath, Path: m.statePath})
add(panes.Field{Label: "cache", Value: m.cachePath, Path: m.cachePath})
// Short, without https://; y copies and o opens the whole URL.
repo := strings.TrimPrefix(update.DefaultRepo, "https://")
add(panes.Field{Label: "docs", Value: repo + "/…/docs", URL: update.DefaultRepo + "/src/branch/main/docs"})
add(panes.Field{Label: "source", Value: repo, URL: update.DefaultRepo})
add(panes.Field{Label: "issues", Value: repo + "/issues", URL: update.DefaultRepo + "/issues"})
add(panes.Field{Label: "releases", Value: repo + "/releases", URL: update.DefaultRepo + "/releases"})
add(panes.Field{Label: "changelog", Value: repo + "/…/CHANGELOG.md", URL: update.DefaultRepo + "/src/branch/main/CHANGELOG.md"})
return append(out, m.changelog...)
}
// updateStatus says whether a newer release is out, and when it was
// last checked.
func (m Model) updateStatus() string {
switch {
case m.updated != "":
return "updated to " + m.updated + ": restart to use it"
case m.newer != nil:
return m.newer.Tag + " is out" + withKey(m.keys.Update, " · %s updates")
}
at := m.store.UpdateCheck().At
if at.IsZero() {
return ""
}
return "up to date · checked " + ago(m.now().Sub(at))
}
// accountStatus says whether an account's sources loaded.
func (m Model) accountStatus(p provider.Provider) string {
r, done := m.sourceResults[p.ID()]
var auth *provider.AuthError
switch {
case !done:
return "loading…"
case errors.As(r.err, &auth) && len(auth.Login) > 0:
return "sign-in required" + withKey(m.keys.Login, " · %s signs in")
case r.err != nil:
return r.err.Error()
}
noun := map[provider.Kind]string{provider.KindAzure: "subscription", provider.KindDatabricks: "workspace", provider.KindRepo: "repo"}[p.Kind()]
return plural(len(r.sources), cmpOr(noun, "source"))
}
// ago is a duration as people say it: "just now", "5m ago", "2h ago".
func ago(d time.Duration) string {
switch {
case d < time.Minute:
return "just now"
case d < time.Hour:
return fmt.Sprintf("%dm ago", int(d.Minutes()))
case d < 48*time.Hour:
return fmt.Sprintf("%dh ago", int(d.Hours()))
}
return fmt.Sprintf("%dd ago", int(d.Hours()/24))
}
// changelogFields are CHANGELOG.md as rows: each release's version and
// date, then its entries, marked by what kind they are.
func changelogFields(md string) []panes.Field {
var out []panes.Field
kind := ""
for _, line := range strings.Split(md, "\n") {
line = strings.TrimSpace(line)
switch {
case strings.HasPrefix(line, "## "):
version, date, _ := strings.Cut(strings.TrimPrefix(line, "## "), " – ")
out = append(out, panes.Field{Label: version, Value: date, Info: true})
kind = ""
case strings.HasPrefix(line, "### "):
kind = strings.ToLower(strings.TrimPrefix(line, "### "))
case strings.HasPrefix(line, "- ") && len(out) > 0:
text := update.PlainLinks(strings.TrimPrefix(line, "- "))
if kind != "" {
text = kind + ": " + text
}
out = append(out, panes.Field{Value: text})
case line != "" && len(out) > 0 && !strings.HasPrefix(line, "<!--"):
// A release's own words ("Hello world 👋").
out = append(out, panes.Field{Value: update.PlainLinks(line)})
}
}
return out
}