diff --git a/internal/ui/app/app.go b/internal/ui/app/app.go index 2e1eee2..93c28a0 100644 --- a/internal/ui/app/app.go +++ b/internal/ui/app/app.go @@ -389,7 +389,7 @@ func New(providers []provider.Provider, opts Options) Model { if opts.Keys != nil { m.keys = *opts.Keys } - m.detail.RevealKey, m.detail.VersionsKey = keys.Name(m.keys.Reveal), keys.Name(m.keys.Versions) + m.detail.RevealKey, m.detail.VersionsKey, m.detail.CopyKey = keys.Name(m.keys.Reveal), keys.Name(m.keys.Versions), keys.Name(m.keys.Copy) m.revealTimeout, m.hideOnBlur = opts.RevealTimeout, opts.HideOnBlur m.expiryWarning = opts.ExpiryWarning m.handlerSetup = opts.HandlerSetup diff --git a/internal/ui/app/app_test.go b/internal/ui/app/app_test.go index d952d6b..c1e89eb 100644 --- a/internal/ui/app/app_test.go +++ b/internal/ui/app/app_test.go @@ -204,7 +204,7 @@ func TestObjectTypeTabs(t *testing.T) { t.Fatalf("selected = %q", s) } screen := d.screen() - for _, want := range []string{"key type RSA 2048", "ops sign, verify", "space to show public key"} { + for _, want := range []string{"key type RSA 2048", "ops sign, verify", "space to show the public key"} { if !strings.Contains(screen, want) { t.Errorf("missing %q:\n%s", want, screen) } @@ -979,7 +979,7 @@ func TestRebindingShowsEverywhere(t *testing.T) { d := newDriverOpts(t, Options{Keys: &km}) d.keys("3") scr := d.screen() - if !strings.Contains(scr, "(z to reveal)") || strings.Contains(scr, "space to reveal") { + if !strings.Contains(scr, "z to reveal") || strings.Contains(scr, "space to reveal") { t.Errorf("detail hint doesn't follow the binding:\n%s", scr) } if !strings.Contains(lastLine(scr), "z reveal/hide") { diff --git a/internal/ui/app/testdata/TestGolden.golden b/internal/ui/app/testdata/TestGolden.golden index 230128d..1e4db37 100644 --- a/internal/ui/app/testdata/TestGolden.golden +++ b/internal/ui/app/testdata/TestGolden.golden @@ -11,13 +11,13 @@ │ kv-restricted ││ ref @Microsoft.KeyVault(SecretUri=https://kv-app-prod.vau│ │ ││ lt.azure.net/secrets/db-password) │ │ ││ │ -│ ││ value •••••••• (space to reveal) │ -│ ││ │ -╰────────────────────── 1 of 3 ─╯│ versions 3 · v to browse │ -╭─ [3] Secrets - Keys - Certs ──╮│ ▸ 3f9c2a 2026-09-01 current │ -│ db-password ││ 91d3e0 2026-03-01 expired │ -│ api-key ││ 0a77c4 2025-09-01 disabled │ -│ legacy-token (disabled) ││ │ +│ ││ 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 │ │ ││ │ │ ││ │ │ ││ │ diff --git a/internal/ui/panes/detail.go b/internal/ui/panes/detail.go index dbdab8c..ed618c7 100644 --- a/internal/ui/panes/detail.go +++ b/internal/ui/panes/detail.go @@ -31,7 +31,7 @@ type Detail struct { Icons icons.Set // RevealKey and VersionsKey are the keys hints mention ("space to // reveal"); a hint is left out when its key is "" (unbound). - RevealKey, VersionsKey string + RevealKey, VersionsKey, CopyKey string secret *provider.Object container *provider.Container @@ -70,7 +70,7 @@ func (d *Detail) ShowContainer(c provider.Container) { // Clear drops the selection and any fetched value. func (d *Detail) Clear() { - *d = Detail{Now: d.Now, Icons: d.Icons, RevealKey: d.RevealKey, VersionsKey: d.VersionsKey, w: d.w, h: d.h} // drops the note too + *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 @@ -284,24 +284,35 @@ func (d Detail) lines(st styles.Styles) []string { } // Secrets are masked; keys and certificates only expose public material. - keyHint := func(k, what string) string { - if k == "" { - return "" + // What the keys do with the value, on the line under it. + hints := func(reveal string, more ...string) string { + var h []string + if d.RevealKey != "" { + h = append(h, d.RevealKey+" to "+reveal) } - return "(" + k + " to " + what + ")" + if d.CopyKey != "" { + h = append(h, d.CopyKey+" to copy") + } + return muted(strings.Join(append(h, more...), " · ")) } - label, hidden := "value", st.Masked.Render("••••••••")+muted(" "+keyHint(d.RevealKey, "reveal")) + label, hidden, reveal := "value", st.Masked.Render("••••••••"), "reveal" switch s.Type { case provider.TypeKey: - label, hidden = "public", muted(keyHint(d.RevealKey, "show public key")) + label, hidden, reveal = "public", "", "show the public key" case provider.TypeCertificate: - label, hidden = "cert", muted(keyHint(d.RevealKey, "show certificate")) + label, hidden, reveal = "cert", "", "show the certificate" } + indent := strings.Repeat(" ", 12) out = append(out, "") switch d.state { case ValueHidden: - field(label, hidden) + if hidden != "" { + field(label, hidden) + out = append(out, indent+hints(reveal)) + } else { + field(label, hints(reveal)) + } case ValueLoading: field(label, spinnerPlaceholder+" fetching…") case ValueFailed: @@ -322,18 +333,11 @@ func (d Detail) lines(st styles.Styles) []string { } } case ValueShown: - var hints []string - if d.RevealKey != "" { - hints = append(hints, d.RevealKey+" to hide") - } + var more []string if d.hidesIn != "" { - hints = append(hints, "hides in "+d.hidesIn) + more = append(more, "hides in "+d.hidesIn) } - line := " " + st.Label.Render(label) - if len(hints) > 0 { - line += muted(" (" + strings.Join(hints, " · ") + ")") - } - out = append(out, line) + out = append(out, " "+st.Label.Render(fmt.Sprintf("%-10s", label))+" "+hints("hide", more...)) for _, line := range strings.Split(d.value.Reveal(), "\n") { for _, w := range strings.Split(ansi.Hardwrap(line, inner, true), "\n") { out = append(out, " "+st.Value.Render(w))