From e64748e85d6ae6f4a6826d4a539714f6acaa1d6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karl-Einar=20Bergstr=C3=B8m?= Date: Sun, 11 Oct 2026 10:32:46 +0200 Subject: [PATCH] refactor: the account pane becomes Fields, the pane for anything but a secret Co-Authored-By: Claude Opus 5.5 (1M context) --- internal/ui/app/app.go | 42 +++++++++---------- .../ui/app/{source_detail.go => fields.go} | 14 +++---- internal/ui/app/help.go | 2 +- internal/ui/app/layout.go | 6 +-- .../ui/panes/{source_detail.go => fields.go} | 26 ++++++------ 5 files changed, 46 insertions(+), 44 deletions(-) rename internal/ui/app/{source_detail.go => fields.go} (83%) rename internal/ui/panes/{source_detail.go => fields.go} (61%) diff --git a/internal/ui/app/app.go b/internal/ui/app/app.go index f92fd90..fc44701 100644 --- a/internal/ui/app/app.go +++ b/internal/ui/app/app.go @@ -163,13 +163,13 @@ type Model struct { clip Clipboard clipClear time.Duration openURL func(string) error - // srcDetail is the fourth pane on a provider's row, with the groups - // of each provider's account as they load (source_detail.go). - srcDetail panes.SourceDetail - groups map[string]groupsState - getenv func(string) string - nvimOpen func(server, path string, line int) error // edit.go - now func() time.Time + // fields is the fourth pane for anything but a secret (now: an + // account), with each account's groups as they load (fields.go). + fields panes.Fields + groups map[string]groupsState + getenv func(string) string + nvimOpen func(server, path string, line int) error // edit.go + now func() time.Time // The cache file, whether the cache changed since it was saved, and // when each provider's Sources list was loaded (persist.go). cachePath string @@ -361,7 +361,7 @@ func New(providers []provider.Provider, opts Options) Model { nvimOpen: nvimOpen, cache: newCache(), groups: map[string]groupsState{}, - srcDetail: panes.NewSourceDetail(), + fields: panes.NewFields(), } if m.clip == nil { m.clip = clipboard.New() @@ -732,7 +732,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.askHandlerSetup() m.keys.Login.SetEnabled(m.loginFor() != nil) m.detail.SetMessage(m.selectedProblem()) - cmds = append(cmds, m.syncSourceDetail()) + cmds = append(cmds, m.syncFields()) if m.anyLoading() && !m.spinning { m.spinning = true cmds = append(cmds, m.spin.Tick) @@ -799,12 +799,12 @@ func (m *Model) handleKey(msg tea.KeyPressMsg) (cmd tea.Cmd, quit bool) { m.jump(false) case key.Matches(msg, k.NextPane): m.focus = (m.focus + 1) % paneCount - if m.focus == paneContainers && m.srcDetail.Shown() { + if m.focus == paneContainers && m.fields.Shown() { m.focus = paneDetail // an account's row: nothing between } case key.Matches(msg, k.PrevPane): m.focus = (m.focus + paneCount - 1) % paneCount - if m.focus == paneSecrets && m.srcDetail.Shown() { + if m.focus == paneSecrets && m.fields.Shown() { m.focus = paneSources } case key.Matches(msg, k.Pane1): @@ -929,8 +929,8 @@ func (m *Model) focusedFilter() filterable { case paneSecrets: return &m.secrets case paneDetail: - if m.srcDetail.Shown() { - return &m.srcDetail + if m.fields.Shown() { + return &m.fields } } return nil @@ -946,8 +946,8 @@ func (m *Model) move(delta int) { case paneSecrets: m.secrets.Move(delta) case paneDetail: - if m.srcDetail.Shown() { - m.srcDetail.Move(delta) + if m.fields.Shown() { + m.fields.Move(delta) } else { m.detail.Scroll(delta) } @@ -970,10 +970,10 @@ func (m *Model) jump(top bool) { m.secrets.Top() case m.focus == paneSecrets: m.secrets.Bottom() - case m.focus == paneDetail && m.srcDetail.Shown() && top: - m.srcDetail.Top() - case m.focus == paneDetail && m.srcDetail.Shown(): - m.srcDetail.Bottom() + case m.focus == paneDetail && m.fields.Shown() && top: + m.fields.Top() + case m.focus == paneDetail && m.fields.Shown(): + m.fields.Bottom() case m.focus == paneDetail && top: m.detail.ScrollTop() case m.focus == paneDetail: @@ -1484,7 +1484,7 @@ func (m *Model) copyValue() tea.Cmd { if a, ok := m.selectedAccount(); ok && a.Copy != "" && m.focus == paneSources { return m.copyText(a.CopyLabel, a.Copy, false) } - if f, ok := m.srcDetail.Selected(); ok && m.focus == paneDetail && m.srcDetail.Shown() { + if f, ok := m.fields.Selected(); ok && m.focus == paneDetail && m.fields.Shown() { return m.copyText(fieldName(f), f.Value, false) } return nil @@ -1612,7 +1612,7 @@ func (m Model) footerView() string { } else if l != nil && l.Filtering() { help = m.helpBar(keys.FilterHelp{Map: m.keys}.ShortHelp(), 2) } else { - help = m.helpBar(contextHelp{k: m.keys, focus: m.focus, commands: len(m.commandsHere()) > 0, account: m.srcDetail.Shown()}.ShortHelp(), 2) + help = m.helpBar(contextHelp{k: m.keys, focus: m.focus, commands: len(m.commandsHere()) > 0, account: m.fields.Shown()}.ShortHelp(), 2) } var lines []string if !m.clipDeadline.IsZero() { diff --git a/internal/ui/app/source_detail.go b/internal/ui/app/fields.go similarity index 83% rename from internal/ui/app/source_detail.go rename to internal/ui/app/fields.go index 01e60f6..a85c58a 100644 --- a/internal/ui/app/source_detail.go +++ b/internal/ui/app/fields.go @@ -12,9 +12,9 @@ import ( "git.kebcloud.no/keb/lazysecrets/internal/ui/panes" ) -// On a provider's row, the fourth pane describes its account: the fields -// the provider knows locally, then the groups the account is in, loaded -// once a session when first shown. +// 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 @@ -28,12 +28,12 @@ type groupsMsg struct { err error } -// syncSourceDetail fills the pane for the selected row, loading the +// syncFields fills the Fields pane for what's selected, loading an // account's groups the first time. -func (m *Model) syncSourceDetail() tea.Cmd { +func (m *Model) syncFields() tea.Cmd { acct, ok := m.selectedAccount() if !ok { - m.srcDetail.Set("", nil) + m.fields.Set("", nil) return nil } r, _ := m.sources.Selected() @@ -57,7 +57,7 @@ func (m *Model) syncSourceDetail() tea.Cmd { } fields = append(fields, groupFields(st)...) } - m.srcDetail.Set(m.byID[r.provider].Label(), fields) + m.fields.Set(m.byID[r.provider].Label(), fields) return cmd } diff --git a/internal/ui/app/help.go b/internal/ui/app/help.go index 71760ce..641ae9f 100644 --- a/internal/ui/app/help.go +++ b/internal/ui/app/help.go @@ -36,7 +36,7 @@ type contextHelp struct { k keys.Map focus pane commands bool // custom commands apply here - account bool // the fourth pane describes an account (source_detail.go) + account bool // the fourth pane describes an account (fields.go) } func (c contextHelp) ShortHelp() []key.Binding { diff --git a/internal/ui/app/layout.go b/internal/ui/app/layout.go index b796653..9f1a8ff 100644 --- a/internal/ui/app/layout.go +++ b/internal/ui/app/layout.go @@ -166,7 +166,7 @@ func (m *Model) layout() { m.containers.SetSize(leftW, h[1]) m.secrets.SetSize(leftW, h[2]) m.detail.SetSize(m.w-leftW, avail) - m.srcDetail.SetSize(m.w-leftW, avail) + m.fields.SetSize(m.w-leftW, avail) } // active reports whether p is drawn focused: it has focus and the terminal @@ -181,8 +181,8 @@ func (m Model) panesView() string { func() string { return m.containers.View(m.st, m.active(paneContainers), spin) }, func() string { return m.secrets.View(m.st, m.active(paneSecrets), spin) }, func() string { - if m.srcDetail.Shown() { - return m.srcDetail.View(m.st, m.active(paneDetail), spin) + if m.fields.Shown() { + return m.fields.View(m.st, m.active(paneDetail), spin) } return m.detail.View(m.st, m.active(paneDetail), spin) }, diff --git a/internal/ui/panes/source_detail.go b/internal/ui/panes/fields.go similarity index 61% rename from internal/ui/panes/source_detail.go rename to internal/ui/panes/fields.go index 00d39fd..0859c46 100644 --- a/internal/ui/panes/source_detail.go +++ b/internal/ui/panes/fields.go @@ -8,10 +8,12 @@ import ( "git.kebcloud.no/keb/lazysecrets/internal/ui/styles" ) -// SourceDetail is the fourth pane on a provider's row: what's known about -// its account, one field per row, to move through, filter and copy from. -// It's a List, so / filters it like the other panes. -type SourceDetail struct { +// Fields is the fourth pane for anything but a secret (an account, and +// what describes vaults, scopes and files): labelled values, one per row, +// a label on the first of several values, to move through, filter and +// copy from. It's a List, so / filters it like the other panes. Secrets +// have Detail, for revealing and versions. +type Fields struct { List[Field] } @@ -22,7 +24,7 @@ type Field struct { Info bool } -func NewSourceDetail() SourceDetail { +func NewFields() Fields { l := NewList("", Item[Field]{ ID: func(f Field) string { return f.Label + "\x00" + f.Value }, Filter: func(f Field) string { return f.Value }, @@ -36,13 +38,13 @@ func NewSourceDetail() SourceDetail { return ansi.Truncate(st.Label.Render(fmt.Sprintf("%-10s", f.Label))+" "+value, width, "…") }, }) - l.Empty = "nothing known about this account" - return SourceDetail{l} + l.Empty = "nothing to show" + return Fields{l} } -// Set shows fields under title. Another title (another account) starts -// over, without a filter. -func (s *SourceDetail) Set(title string, fields []Field) { +// Set shows fields under title. Another title (another account or vault) +// starts over, without a filter. +func (s *Fields) Set(title string, fields []Field) { if title != s.Title { s.Reset() s.Title = title @@ -53,10 +55,10 @@ func (s *SourceDetail) Set(title string, fields []Field) { } // Shown reports whether there's anything to show. -func (s SourceDetail) Shown() bool { return len(s.Items()) > 0 } +func (s Fields) Shown() bool { return len(s.Items()) > 0 } // Selected is the field under the cursor, unless it's an info row. -func (s SourceDetail) Selected() (Field, bool) { +func (s Fields) Selected() (Field, bool) { f, ok := s.List.Selected() if !ok || f.Info { return Field{}, false