feat: the right pane describes what's selected in the pane you're in (#9)
The right pane kept showing the selected secret when you moved to Vaults or Sources. Now it follows focus: | focus | right pane | |---|---| | Sources | the account (group row), or the subscription / workspace / repo: name, note, ID / profile / path, host or remote, account, web page, vault count | | Vaults | the vault / scope / file: name, note, URI / backend / format, source, web page, counts of loaded secrets, keys, certificates | | Secrets | the secret, as before | | the right pane itself | what it described when you went in; `shift+tab` goes back there | All but the secret use the `Fields` pane (#7), so `j`/`k`, `/` and `y` work on them; `Detail` loses the error message it got in #4, which is now a red "problem" row in `Fields`. A value revealed from a left pane stays in view until hidden, so `space` from any pane still shows it. `tab` from a subscription goes to Vaults as before; only an account row skips to its details. This also changes the startup view: focus starts in Sources, so the right pane describes the first subscription instead of the first secret (golden output updated). Tests that read the secret pane focus Secrets first; a new test walks secret → vault → subscription → into the pane → back, and reveals from Sources. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Karl-Einar Bergstrøm <karl-einarb@outlook.com> Reviewed-on: #9
This commit was merged in pull request #9.
This commit is contained in:
1 parent
c8abc1dc95
commit
1c8d69d957
9 files changed
+232
-67
No files matched your search
+12
-7
@@ -163,9 +163,11 @@ type Model struct {
|
||||
clip Clipboard
|
||||
clipClear time.Duration
|
||||
openURL func(string) error
|
||||
// fields is the fourth pane for anything but a secret (now: an
|
||||
// account), with each account's groups as they load (fields.go).
|
||||
// fields is the fourth pane for anything but a secret, with each
|
||||
// account's groups as they load; lastLeft is the left pane it
|
||||
// describes while the fourth has focus (fields.go).
|
||||
fields panes.Fields
|
||||
lastLeft pane
|
||||
groups map[string]groupsState
|
||||
getenv func(string) string
|
||||
nvimOpen func(server, path string, line int) error // edit.go
|
||||
@@ -731,7 +733,9 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
m.waiting = 0
|
||||
m.askHandlerSetup()
|
||||
m.keys.Login.SetEnabled(m.loginFor() != nil)
|
||||
m.detail.SetMessage(m.selectedProblem())
|
||||
if m.focus != paneDetail {
|
||||
m.lastLeft = m.focus
|
||||
}
|
||||
cmds = append(cmds, m.syncFields())
|
||||
if m.anyLoading() && !m.spinning {
|
||||
m.spinning = true
|
||||
@@ -799,13 +803,14 @@ 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.fields.Shown() {
|
||||
if r, ok := m.sources.Selected(); ok && m.focus == paneContainers && r.kind == rowGroup {
|
||||
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.fields.Shown() {
|
||||
m.focus = paneSources
|
||||
if m.focus == paneDetail {
|
||||
m.focus = m.lastLeft // back to what it describes
|
||||
} else {
|
||||
m.focus = (m.focus + paneCount - 1) % paneCount
|
||||
}
|
||||
case key.Matches(msg, k.Pane1):
|
||||
m.focus = paneSources
|
||||
|
||||
@@ -185,8 +185,9 @@ func TestIcons(t *testing.T) {
|
||||
if !strings.Contains(screen, azure+" Azure") || !strings.Contains(screen, dbx+" Databricks") {
|
||||
t.Errorf("icons missing:\n%s", screen)
|
||||
}
|
||||
if !strings.Contains(screen, "source "+azure+" Azure / platform-prod") {
|
||||
t.Errorf("detail source line missing icon:\n%s", screen)
|
||||
d.keys("3")
|
||||
if !strings.Contains(d.screen(), "source "+azure+" Azure / platform-prod") {
|
||||
t.Errorf("detail source line missing icon:\n%s", d.screen())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -400,6 +401,7 @@ func (describing) Describe(_ context.Context, o provider.Object) (provider.Objec
|
||||
func TestDescribeUpdatesDetail(t *testing.T) {
|
||||
t.Parallel()
|
||||
d := newDriver(t, describing{fake.Azure(0)})
|
||||
d.keys("3") // the fourth pane follows focus: the secret
|
||||
if !strings.Contains(d.screen(), "described db-password") {
|
||||
t.Fatalf("describe result not shown:\n%s", d.screen())
|
||||
}
|
||||
@@ -487,6 +489,7 @@ func TestVersions(t *testing.T) {
|
||||
func TestVersionHistoryInDetail(t *testing.T) {
|
||||
t.Parallel()
|
||||
d := newDriver(t)
|
||||
d.keys("3") // the fourth pane follows focus: the secret
|
||||
screen := d.screen()
|
||||
for _, want := range []string{
|
||||
"versions 3 · v to browse",
|
||||
@@ -594,7 +597,7 @@ func TestPermissionErrorShownInPane(t *testing.T) {
|
||||
if !strings.Contains(d.screen(), "403 Forbidden") {
|
||||
t.Error("error not rendered")
|
||||
}
|
||||
if !strings.Contains(d.screen(), "Select a secret") {
|
||||
if !strings.Contains(d.screen(), "name kv-restricted") {
|
||||
t.Error("detail should show the container summary")
|
||||
}
|
||||
}
|
||||
@@ -1037,7 +1040,7 @@ func TestSignInMessageOnceThenInDetails(t *testing.T) {
|
||||
t.Parallel()
|
||||
d := newDriverOpts(t, Options{CollapseGroups: false}, fake.Azure(0), signedOut{fake.Azure(0)})
|
||||
hint := "Azure · admin: sign-in required"
|
||||
if !strings.Contains(lastLines(d.screen(), 2), hint) {
|
||||
if !strings.Contains(lastLines(d.screen(), 2), hint) { // the footer: one line
|
||||
t.Fatalf("not said at start:\n%s", d.screen())
|
||||
}
|
||||
d.keys("j")
|
||||
@@ -1050,7 +1053,7 @@ func TestSignInMessageOnceThenInDetails(t *testing.T) {
|
||||
if r, _ := d.m.sources.Selected(); r.provider != "Azure:admin@contoso.com/t1" {
|
||||
t.Fatalf("selected %+v", r)
|
||||
}
|
||||
if !strings.Contains(d.screen(), hint) || !strings.Contains(d.screen(), "press L to run az login --tenant t1") {
|
||||
if scr := d.screen(); !strings.Contains(scr, "problem") || !strings.Contains(scr, hint) || !strings.Contains(scr, "press L to run az login") {
|
||||
t.Errorf("not in Details:\n%s", d.screen())
|
||||
}
|
||||
if a := d.m.loginFor(); a == nil || a.Account != "Azure · admin" {
|
||||
@@ -1154,3 +1157,35 @@ func TestAccountPane(t *testing.T) {
|
||||
t.Errorf("back to %v, want Sources", d.m.focus)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetailsFollowFocus(t *testing.T) {
|
||||
t.Parallel()
|
||||
d := newDriver(t)
|
||||
d.keys("3") // a secret
|
||||
if !strings.Contains(d.screen(), "version 3f9c2a") {
|
||||
t.Fatalf("secret not shown:\n%s", d.screen())
|
||||
}
|
||||
d.keys("2") // its vault
|
||||
if scr := d.screen(); !strings.Contains(scr, "name kv-app-prod") || strings.Contains(scr, "version 3f9c2a") {
|
||||
t.Errorf("vault not described:\n%s", scr)
|
||||
}
|
||||
d.keys("1") // its subscription
|
||||
if scr := d.screen(); !strings.Contains(scr, "name platform-prod") || !strings.Contains(scr, "vaults 3") {
|
||||
t.Errorf("subscription not described:\n%s", scr)
|
||||
}
|
||||
// Into the fourth pane: it keeps describing the subscription, and
|
||||
// shift+tab goes back there.
|
||||
d.keys("4")
|
||||
if !strings.Contains(d.screen(), "name platform-prod") {
|
||||
t.Errorf("lost the subscription:\n%s", d.screen())
|
||||
}
|
||||
d.keys("shift+tab")
|
||||
if d.m.focus != paneSources {
|
||||
t.Errorf("back to %v", d.m.focus)
|
||||
}
|
||||
// A value revealed from a left pane stays in view until hidden.
|
||||
d.keys("space")
|
||||
if !strings.Contains(d.screen(), "correct-horse-battery-staple") {
|
||||
t.Errorf("value not shown:\n%s", d.screen())
|
||||
}
|
||||
}
|
||||
@@ -421,8 +421,9 @@ func TestStarIsPerAccount(t *testing.T) {
|
||||
d.keys("n")
|
||||
d.typeText("shared note")
|
||||
d.keys("enter")
|
||||
if n := strings.Count(d.screen(), "shared note"); n != 2 {
|
||||
t.Errorf("note shown %d times, want 2:\n%s", n, d.screen())
|
||||
// On both accounts' rows, and in the selected one's details.
|
||||
if n := strings.Count(d.screen(), "shared note"); n != 3 {
|
||||
t.Errorf("note shown %d times, want 3:\n%s", n, d.screen())
|
||||
}
|
||||
d.keys("F")
|
||||
var labels []string
|
||||
|
||||
+132
-10
@@ -7,6 +7,7 @@ import (
|
||||
"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"
|
||||
@@ -28,21 +29,67 @@ type groupsMsg struct {
|
||||
err error
|
||||
}
|
||||
|
||||
// syncFields fills the Fields pane for what's selected, loading an
|
||||
// account's groups the first time.
|
||||
// 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 {
|
||||
acct, ok := m.selectedAccount()
|
||||
if !ok {
|
||||
m.fields.Set("", nil)
|
||||
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
|
||||
}
|
||||
r, _ := m.sources.Selected()
|
||||
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 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
|
||||
for _, f := range acct.Fields {
|
||||
fields = append(fields, panes.Field{Label: f.Label, Value: f.Value})
|
||||
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 := m.byID[r.provider].(provider.AccountGroups); ok {
|
||||
if g, ok := p.(provider.AccountGroups); ok {
|
||||
st, seen := m.groups[r.provider]
|
||||
if !seen {
|
||||
st = groupsState{loading: true}
|
||||
@@ -57,10 +104,85 @@ func (m *Model) syncFields() tea.Cmd {
|
||||
}
|
||||
fields = append(fields, groupFields(st)...)
|
||||
}
|
||||
m.fields.Set(m.byID[r.provider].Label(), fields)
|
||||
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") {
|
||||
|
||||
@@ -42,7 +42,7 @@ func TestNotesOnRows(t *testing.T) {
|
||||
d.note("main prod")
|
||||
|
||||
screen := d.screen()
|
||||
for _, want := range []string{"stripe live", "app secrets", "main prod", "note stripe live"} {
|
||||
for _, want := range []string{"stripe live", "app secrets", "main prod", "note main prod"} {
|
||||
if !strings.Contains(screen, want) {
|
||||
t.Errorf("missing %q:\n%s", want, screen)
|
||||
}
|
||||
|
||||
+19
-19
@@ -1,23 +1,23 @@
|
||||
╭─ [1] Sources - Favourites ────╮╭─ Secret ────────────────────────────────────────────────────────╮
|
||||
│ ▾ Azure (2) ││ name db-password │
|
||||
│ platform-prod ││ vault kv-app-prod │
|
||||
│ platform-dev ││ source Azure / platform-prod │
|
||||
│ ▾ Databricks (2) ││ version 3f9c2a │
|
||||
│ prod ││ enabled yes │
|
||||
╰────────────────────── 2 of 6 ─╯│ updated 2026-09-01 09:30 │
|
||||
╭─ [2] Vaults ──────────────────╮│ expires 2027-09-01 09:30 │
|
||||
│ kv-app-prod ││ content text/plain │
|
||||
│ kv-shared-prod ││ tags env=prod, owner=data │
|
||||
│ kv-restricted ││ ref @Microsoft.KeyVault(SecretUri=https://kv-app-prod.vau│
|
||||
│ ││ lt.azure.net/secrets/db-password) │
|
||||
╭─ [1] Sources - Favourites ────╮╭─ platform-prod ─────────────────────────────────────────────────╮
|
||||
│ ▾ Azure (2) ││ name platform-prod │
|
||||
│ platform-prod ││ ID sub-prod │
|
||||
│ platform-dev ││ web https://portal.azure.com/#resource/subscriptions/sub…│
|
||||
│ ▾ Databricks (2) ││ vaults 3 │
|
||||
│ prod ││ │
|
||||
╰────────────────────── 2 of 6 ─╯│ │
|
||||
╭─ [2] Vaults ──────────────────╮│ │
|
||||
│ kv-app-prod ││ │
|
||||
│ kv-shared-prod ││ │
|
||||
│ kv-restricted ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ value •••••••• │
|
||||
│ ││ space to reveal · y to copy │
|
||||
╰────────────────────── 1 of 3 ─╯│ │
|
||||
╭─ [3] Secrets - Keys - Certs ──╮│ versions 3 · v to browse │
|
||||
│ db-password ││ ▸ 3f9c2a 2026-09-01 current │
|
||||
│ api-key ││ 91d3e0 2026-03-01 expired │
|
||||
│ legacy-token (disabled) ││ 0a77c4 2025-09-01 disabled │
|
||||
╭─ [3] Secrets - Keys - Certs ──╮│ │
|
||||
│ db-password ││ │
|
||||
│ api-key ││ │
|
||||
│ legacy-token (disabled) ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
@@ -26,5 +26,5 @@
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
╰────────────────────── 1 of 3 ─╯╰─────────────────────────────────────────────────────────────────╯
|
||||
╰────────────────────── 1 of 3 ─╯╰──────────────────────────────────────────────────────── 1 of 4 ─╯
|
||||
enter open • ctrl+s search • s copy link • S share… • f star • F favourites • … • ? help • q quit
|
||||
@@ -46,7 +46,6 @@ type Detail struct {
|
||||
|
||||
versions []provider.Object // newest first; nil while unknown
|
||||
note string // the user's note on what's shown
|
||||
message string // shown instead of the hint when nothing is selected: an error
|
||||
|
||||
offset int
|
||||
w, h int
|
||||
@@ -72,10 +71,6 @@ func (d *Detail) Clear() {
|
||||
*d = Detail{Now: d.Now, Icons: d.Icons, RevealKey: d.RevealKey, VersionsKey: d.VersionsKey, CopyKey: d.CopyKey, w: d.w, h: d.h} // drops the note too
|
||||
}
|
||||
|
||||
// SetMessage sets what's shown instead of the hint when nothing is
|
||||
// selected, such as why an account has nothing to show; "" for the hint.
|
||||
func (d *Detail) SetMessage(s string) { d.message = s }
|
||||
|
||||
// SetDescribing marks extra metadata as being fetched.
|
||||
func (d *Detail) SetDescribing() { d.describing, d.describeErr = true, nil }
|
||||
|
||||
@@ -191,12 +186,6 @@ func (d Detail) lines(st styles.Styles) []string {
|
||||
muted := st.Muted.Render
|
||||
|
||||
if d.container == nil {
|
||||
if d.message != "" {
|
||||
for _, l := range strings.Split(ansi.Hardwrap(d.message, max(1, inner-2), true), "\n") {
|
||||
out = append(out, " "+st.Error.Render(l))
|
||||
}
|
||||
return out
|
||||
}
|
||||
return []string{muted(" Select a vault or scope to browse its secrets.")}
|
||||
}
|
||||
c := *d.container
|
||||
|
||||
+18
-12
@@ -15,13 +15,14 @@ import (
|
||||
// have Detail, for revealing and versions.
|
||||
type Fields struct {
|
||||
List[Field]
|
||||
shown bool
|
||||
}
|
||||
|
||||
// Field is a row: a value under a label. Info rows (loading, errors)
|
||||
// can't be copied.
|
||||
// can't be copied; Err ones are errors.
|
||||
type Field struct {
|
||||
Label, Value string
|
||||
Info bool
|
||||
Info, Err bool
|
||||
}
|
||||
|
||||
func NewFields() Fields {
|
||||
@@ -32,30 +33,35 @@ func NewFields() Fields {
|
||||
FilterExtra: func(f Field) string { return f.Label },
|
||||
Render: func(st styles.Styles, f Field, width int) string {
|
||||
value := f.Value
|
||||
if f.Info {
|
||||
switch {
|
||||
case f.Err:
|
||||
value = st.Error.Render(value)
|
||||
case f.Info:
|
||||
value = st.Muted.Render(value)
|
||||
}
|
||||
return ansi.Truncate(st.Label.Render(fmt.Sprintf("%-10s", f.Label))+" "+value, width, "…")
|
||||
},
|
||||
})
|
||||
l.Empty = "nothing to show"
|
||||
return Fields{l}
|
||||
return Fields{List: l}
|
||||
}
|
||||
|
||||
// Set shows fields under title. Another title (another account or vault)
|
||||
// starts over, without a filter.
|
||||
func (s *Fields) Set(title string, fields []Field) {
|
||||
// Show shows fields under title, instead of the secret pane. Another
|
||||
// title (another account or vault) starts over, without a filter.
|
||||
func (s *Fields) Show(title string, fields []Field) {
|
||||
if title != s.Title {
|
||||
s.Reset()
|
||||
s.Title = title
|
||||
}
|
||||
if len(fields) > 0 || len(s.Items()) > 0 {
|
||||
s.SetItems(fields)
|
||||
}
|
||||
s.shown = true
|
||||
s.SetItems(fields)
|
||||
}
|
||||
|
||||
// Shown reports whether there's anything to show.
|
||||
func (s Fields) Shown() bool { return len(s.Items()) > 0 }
|
||||
// Hide gives the fourth pane back to the secret pane.
|
||||
func (s *Fields) Hide() { s.shown = false }
|
||||
|
||||
// Shown reports whether the fourth pane shows Fields.
|
||||
func (s Fields) Shown() bool { return s.shown }
|
||||
|
||||
// Selected is the field under the cursor, unless it's an info row.
|
||||
func (s Fields) Selected() (Field, bool) {
|
||||
|
||||
Reference in new issue
Block a user