Files
lazysecrets/internal/ui/app/fields.go
T
kebandClaude Opus 5.5 036f32d9ac
CI / Checks (pull_request) Successful in 21s
PR title / Conventional title (pull_request) Successful in 2s
feat: a status pane, like lazygit's: version, updates, accounts, files and links
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-11 11:07:36 +02:00

215 lines
6.0 KiB
Go

package app
import (
"context"
"errors"
"fmt"
"strings"
tea "charm.land/bubbletea/v2"
"github.com/charmbracelet/x/ansi"
"git.kebcloud.no/keb/lazysecrets/internal/provider"
"git.kebcloud.no/keb/lazysecrets/internal/ui/panes"
)
// The fourth pane shows Fields for anything but a secret. On a provider's
// row: its account, from what the provider knows locally, then the
// groups the account is in, loaded once a session when first shown.
type groupsState struct {
names []string
err error
loading bool
}
type groupsMsg struct {
provider string
names []string
err error
}
// detailOf is the pane the fourth pane describes: the one in focus, or,
// with the fourth in focus, the one it was entered from.
func (m Model) detailOf() pane {
if m.focus != paneDetail {
return m.focus
}
return m.lastLeft
}
// syncFields shows the Fields pane for what the focused pane has selected,
// unless that's a secret (or a vault with the Secrets pane in focus), which
// the secret pane shows, as it does a revealed value whatever the focus.
// An account's groups load the first time.
func (m *Model) syncFields() tea.Cmd {
if m.detail.State() != panes.ValueHidden {
// A value revealed (or being fetched) from any pane stays in
// view until it's hidden.
m.fields.Hide()
return nil
}
switch m.detailOf() {
case paneSources:
r, ok := m.sources.Selected()
switch {
case !ok:
m.fields.Show("", nil)
case m.selectedProblem() != "":
m.fields.Show(cmpOr(r.label, r.source.Name), m.problemFields(m.selectedProblem()))
case r.kind == rowGroup:
return m.showAccount(r)
case r.kind == rowSource:
m.fields.Show(r.source.Name, m.sourceFields(r.source))
default:
m.fields.Show("", nil)
}
case paneStatus:
m.fields.Show("lazysecrets", m.statusFields())
case paneContainers:
if c, ok := m.containers.Selected(); ok {
m.fields.Show(c.Name, m.containerFields(c))
} else {
m.fields.Show("", nil)
}
default:
m.fields.Hide()
}
return nil
}
// showAccount shows a provider's account, loading its groups the first
// time.
func (m *Model) showAccount(r sourceRow) tea.Cmd {
p := m.byID[r.provider]
var fields []panes.Field
if a, ok := p.(provider.Accounts); ok {
if acct, ok := a.Account(); ok {
for _, f := range acct.Fields {
fields = append(fields, panes.Field{Label: f.Label, Value: f.Value})
}
}
}
var cmd tea.Cmd
if g, ok := p.(provider.AccountGroups); ok {
st, seen := m.groups[r.provider]
if !seen {
st = groupsState{loading: true}
m.groups[r.provider] = st
id, timeout := r.provider, m.timeout
cmd = func() tea.Msg {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
names, err := g.Groups(ctx)
return groupsMsg{provider: id, names: names, err: err}
}
}
fields = append(fields, groupFields(st)...)
}
m.fields.Show(p.Label(), fields)
return cmd
}
// sourceFields describe a subscription, workspace or repo.
func (m Model) sourceFields(src provider.Source) []panes.Field {
var out []panes.Field
add := func(label, value string) {
if value != "" {
out = append(out, panes.Field{Label: label, Value: value})
}
}
// Azure's detail is "subscription <ID>": the ID row says it.
id, detail := "ID", ""
switch src.Kind {
case provider.KindDatabricks:
id, detail = "profile", "host"
case provider.KindRepo:
id, detail = "path", "remote"
}
add("name", src.Name)
add("note", m.store.Node(sourceKey(src)).Note)
if src.ID != src.Name {
add(id, src.ID)
}
if detail != "" {
add(detail, src.Detail)
}
if src.ProviderLabel != string(src.Kind) {
add("account", src.ProviderLabel)
}
add("web", src.URL)
if e, ok := m.cache.containers[sourceRowKey(src.Provider, src.ID)]; ok {
label := map[provider.Kind]string{provider.KindDatabricks: "scopes", provider.KindRepo: "files"}[src.Kind]
add(cmpOr(label, "vaults"), fmt.Sprint(len(e.value)))
}
return out
}
// containerFields describe a vault, scope or file.
func (m Model) containerFields(c provider.Container) []panes.Field {
var out []panes.Field
add := func(label, value string) {
if value != "" {
out = append(out, panes.Field{Label: label, Value: value})
}
}
add("name", c.Name)
add("note", m.store.Node(storeKey(c)).Note)
// The vault's URI, the scope's backend, the file's format.
switch c.Source.Kind {
case provider.KindAzure:
add("URI", c.Detail)
case provider.KindDatabricks:
add("backend", c.Detail)
case provider.KindRepo:
add("format", c.Detail)
}
add("source", cmpOr(c.Source.ProviderLabel, string(c.Source.Kind))+" / "+c.Source.Name)
add("web", c.URL)
for _, t := range c.ObjectTypes() {
if e, ok := m.cache.objects[containerKey(c)+"|"+string(t)]; ok {
add(strings.ToLower(string(t))+"s", fmt.Sprint(len(e.value)))
}
}
return out
}
// problemFields show why an account has nothing to show, wrapped to the
// pane, in red.
func (m Model) problemFields(problem string) []panes.Field {
var out []panes.Field
for _, l := range strings.Split(ansi.Wrap(problem, max(20, m.fields.Width()-16), " "), "\n") {
out = append(out, panes.Field{Value: l, Info: true, Err: true})
}
out[0].Label = "problem"
return out
}
// fieldName names a copied field: its label, or for a group, the group.
func fieldName(f panes.Field) string {
if f.Label == "" || strings.HasPrefix(f.Label, "groups") {
return "group " + f.Value
}
return f.Label
}
// groupFields are the groups' rows: labelled on the first.
func groupFields(st groupsState) []panes.Field {
switch {
case st.loading:
return []panes.Field{{Label: "groups", Value: "loading…", Info: true}}
case errors.Is(st.err, provider.ErrUnsupported):
return nil
case st.err != nil:
return []panes.Field{{Label: "groups", Value: "couldn't list them: " + st.err.Error(), Info: true}}
case len(st.names) == 0:
return []panes.Field{{Label: "groups", Value: "none", Info: true}}
}
out := make([]panes.Field, len(st.names))
for i, n := range st.names {
out[i] = panes.Field{Value: n}
}
out[0].Label = fmt.Sprintf("groups %d", len(st.names))
return out
}