Compare commits
1
Commits
v0.2.1
..
114478d2f2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
114478d2f2
|
No files matched your search
@@ -2,12 +2,6 @@
|
|||||||
|
|
||||||
Every release of lazysecrets, newest first.
|
Every release of lazysecrets, newest first.
|
||||||
|
|
||||||
## v0.2.1 – 2026-10-15
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- A link opens lazysecrets with your cache and next to your running instances ([#71](https://git.kebcloud.no/keb/lazysecrets/pulls/71)) ([bb41767](https://git.kebcloud.no/keb/lazysecrets/commit/bb41767cbad00bab4f1824f4e0754cc7abd98c59))
|
|
||||||
|
|
||||||
## v0.2.0 – 2026-10-14
|
## v0.2.0 – 2026-10-14
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 113 KiB After Width: | Height: | Size: 258 KiB |
+12
-9
@@ -8,6 +8,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"image/color"
|
"image/color"
|
||||||
|
"math"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -724,7 +725,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
if m.versionsOf != nil {
|
if m.versionsOf != nil {
|
||||||
m.setVersions(msg.items)
|
m.setVersions(msg.items)
|
||||||
} else {
|
} else {
|
||||||
m.secrets.SetItems(m.starredObjectsFirst(msg.items))
|
m.secrets.SetItems(m.starredFirst(msg.items))
|
||||||
cmds = append(cmds, m.refreshShown(before))
|
cmds = append(cmds, m.refreshShown(before))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1256,7 +1257,7 @@ func (m *Model) showObjects(c provider.Container, key string) tea.Cmd {
|
|||||||
m.secrets.SetLoading()
|
m.secrets.SetLoading()
|
||||||
return m.loadObjects(c, m.objType, key)
|
return m.loadObjects(c, m.objType, key)
|
||||||
}
|
}
|
||||||
m.secrets.SetItems(m.starredObjectsFirst(e.value))
|
m.secrets.SetItems(m.starredFirst(e.value))
|
||||||
m.secrets.Select(m.cache.cursor[key])
|
m.secrets.Select(m.cache.cursor[key])
|
||||||
if e.fresh(m.now()) {
|
if e.fresh(m.now()) {
|
||||||
return nil
|
return nil
|
||||||
@@ -1675,8 +1676,12 @@ func (m *Model) copyText(label, text string, sensitive bool) tea.Cmd {
|
|||||||
|
|
||||||
// clipTick fires when the countdown's displayed second next changes.
|
// clipTick fires when the countdown's displayed second next changes.
|
||||||
func (m *Model) clipTick() tea.Cmd {
|
func (m *Model) clipTick() tea.Cmd {
|
||||||
|
wait := m.clipDeadline.Sub(m.now()) % time.Second
|
||||||
|
if wait <= 0 {
|
||||||
|
wait = time.Second
|
||||||
|
}
|
||||||
token := m.clipToken
|
token := m.clipToken
|
||||||
return tea.Tick(untilNextSecond(m.clipDeadline, m.now()), func(time.Time) tea.Msg { return clipTickMsg{token: token} })
|
return tea.Tick(wait, func(time.Time) tea.Msg { return clipTickMsg{token: token} })
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) cancelClipClear() {
|
func (m *Model) cancelClipClear() {
|
||||||
@@ -1783,15 +1788,13 @@ func (m Model) footerView() string {
|
|||||||
} else if l != nil && l.Filtering() {
|
} else if l != nil && l.Filtering() {
|
||||||
help = m.helpBar(keys.FilterHelp{Map: m.keys}.ShortHelp(), 2)
|
help = m.helpBar(keys.FilterHelp{Map: m.keys}.ShortHelp(), 2)
|
||||||
} else {
|
} else {
|
||||||
reveal := "show"
|
revealed := m.detail.State() == panes.ValueShown
|
||||||
if m.detail.State() == panes.ValueShown {
|
help = m.helpBar(contextHelp{k: m.keys, focus: m.focus, of: m.detailOf(), commands: len(m.commandsHere()) > 0, account: m.fields.Shown(), revealed: &revealed}.ShortHelp(), 2)
|
||||||
reveal = "hide"
|
|
||||||
}
|
|
||||||
help = m.helpBar(contextHelp{k: m.keys, focus: m.focus, of: m.detailOf(), commands: len(m.commandsHere()) > 0, account: m.fields.Shown(), reveal: reveal}.ShortHelp(), 2)
|
|
||||||
}
|
}
|
||||||
var lines []string
|
var lines []string
|
||||||
if !m.clipDeadline.IsZero() {
|
if !m.clipDeadline.IsZero() {
|
||||||
lines = append(lines, m.flashLine(done(fmt.Sprintf("copied %s · clipboard clears in %ds", m.clipLabel, secondsLeft(m.clipDeadline, m.now())))))
|
left := max(0, int(math.Ceil(m.clipDeadline.Sub(m.now()).Seconds())))
|
||||||
|
lines = append(lines, m.flashLine(done(fmt.Sprintf("copied %s · clipboard clears in %ds", m.clipLabel, left))))
|
||||||
}
|
}
|
||||||
if m.flash.text != "" {
|
if m.flash.text != "" {
|
||||||
lines = append(lines, m.flashLine(m.flash))
|
lines = append(lines, m.flashLine(m.flash))
|
||||||
|
|||||||
@@ -306,6 +306,3 @@ func newTextInput(prompt string, width int) textinput.Model {
|
|||||||
in.SetWidth(width)
|
in.SetWidth(width)
|
||||||
return in
|
return in
|
||||||
}
|
}
|
||||||
|
|
||||||
// pathInputWidth fits a link or a folder path in a prompt, in the window.
|
|
||||||
func (m Model) pathInputWidth() int { return max(20, min(m.w-12, 64)) }
|
|
||||||
@@ -116,7 +116,7 @@ func (m Model) containerRender(withSource bool) func(styles.Styles, provider.Con
|
|||||||
line = marks(sty, n, c.Source.Provider, m.recentIcon, m.now()) + line
|
line = marks(sty, n, c.Source.Provider, m.recentIcon, m.now()) + line
|
||||||
if withSource {
|
if withSource {
|
||||||
src := c.Source.Name
|
src := c.Source.Name
|
||||||
if account := accountOf(c.Source); account != "" {
|
if account, ok := strings.CutPrefix(c.Source.ProviderLabel, string(c.Source.Kind)+" · "); ok {
|
||||||
src = account + " / " + src
|
src = account + " / " + src
|
||||||
}
|
}
|
||||||
line += sty.Muted.Render(" · " + src)
|
line += sty.Muted.Render(" · " + src)
|
||||||
@@ -144,36 +144,24 @@ func (m Model) objectRender() func(styles.Styles, provider.Object, int) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// starredFirst moves starred items to the front, keeping the order
|
// starredContainersFirst moves starred vaults and scopes to the front,
|
||||||
// otherwise. It doesn't modify items, which may be cached.
|
// keeping the order otherwise. It doesn't modify items, which may be cached.
|
||||||
func starredFirst[T any](items []T, starred func(T) bool) []T {
|
func (m Model) starredContainersFirst(items []provider.Container) []provider.Container {
|
||||||
rank := func(x T) int {
|
|
||||||
if starred(x) {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
out := slices.Clone(items)
|
out := slices.Clone(items)
|
||||||
slices.SortStableFunc(out, func(a, b T) int { return rank(a) - rank(b) })
|
slices.SortStableFunc(out, func(a, b provider.Container) int {
|
||||||
|
return boolRank(m.store.Node(storeKey(a)).StarredFor(a.Source.Provider)) - boolRank(m.store.Node(storeKey(b)).StarredFor(b.Source.Provider))
|
||||||
|
})
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) starredSourcesFirst(sources []provider.Source) []provider.Source {
|
// starredFirst moves starred objects to the front, keeping the order
|
||||||
return starredFirst(sources, func(s provider.Source) bool {
|
// otherwise. It doesn't modify items, which may be cached.
|
||||||
return m.store.Node(sourceKey(s)).StarredFor(s.Provider)
|
func (m Model) starredFirst(items []provider.Object) []provider.Object {
|
||||||
})
|
out := slices.Clone(items)
|
||||||
}
|
slices.SortStableFunc(out, func(a, b provider.Object) int {
|
||||||
|
return boolRank(m.store.Node(objectKey(a)).StarredFor(a.Container.Source.Provider)) - boolRank(m.store.Node(objectKey(b)).StarredFor(b.Container.Source.Provider))
|
||||||
func (m Model) starredContainersFirst(items []provider.Container) []provider.Container {
|
|
||||||
return starredFirst(items, func(c provider.Container) bool {
|
|
||||||
return m.store.Node(storeKey(c)).StarredFor(c.Source.Provider)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m Model) starredObjectsFirst(items []provider.Object) []provider.Object {
|
|
||||||
return starredFirst(items, func(o provider.Object) bool {
|
|
||||||
return m.store.Node(objectKey(o)).StarredFor(o.Container.Source.Provider)
|
|
||||||
})
|
})
|
||||||
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
// showLinkedContainers lists the vaults opened from links.
|
// showLinkedContainers lists the vaults opened from links.
|
||||||
@@ -211,7 +199,7 @@ func (m *Model) resortStarred() {
|
|||||||
m.containers.SetItems(m.starredContainersFirst(m.containers.Items()))
|
m.containers.SetItems(m.starredContainersFirst(m.containers.Items()))
|
||||||
}
|
}
|
||||||
if m.versionsOf == nil {
|
if m.versionsOf == nil {
|
||||||
m.secrets.SetItems(m.starredObjectsFirst(m.secrets.Items()))
|
m.secrets.SetItems(m.starredFirst(m.secrets.Items()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -257,7 +245,7 @@ func (m *Model) toggleFavourite() tea.Cmd {
|
|||||||
c := o.Container
|
c := o.Container
|
||||||
toggle := func(n *store.Node) { on = n.ToggleStar(c.Source.Provider) }
|
toggle := func(n *store.Node) { on = n.ToggleStar(c.Source.Provider) }
|
||||||
if err = m.remember(objectKey(o), o.Name, []string{c.Source.Name, c.Name}, c.Source, toggle); err == nil {
|
if err = m.remember(objectKey(o), o.Name, []string{c.Source.Name, c.Name}, c.Source, toggle); err == nil {
|
||||||
m.secrets.SetItems(m.starredObjectsFirst(m.secrets.Items()))
|
m.secrets.SetItems(m.starredFirst(m.secrets.Items()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
switch {
|
switch {
|
||||||
|
|||||||
+11
-6
@@ -39,9 +39,10 @@ type contextHelp struct {
|
|||||||
commands bool // custom commands apply here
|
commands bool // custom commands apply here
|
||||||
account bool // the fourth pane shows Fields (fields.go)
|
account bool // the fourth pane shows Fields (fields.go)
|
||||||
of pane // what the fourth pane describes
|
of pane // what the fourth pane describes
|
||||||
// reveal, when set, is what the reveal key will do, "show" or "hide":
|
// revealed, when set, says whether the selected value is shown, so
|
||||||
// the same width so the bar doesn't shift.
|
// the reveal key says just what it'll do: show or hide, the same
|
||||||
reveal string
|
// width so the bar doesn't shift.
|
||||||
|
revealed *bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c contextHelp) ShortHelp() []key.Binding {
|
func (c contextHelp) ShortHelp() []key.Binding {
|
||||||
@@ -69,8 +70,8 @@ func (c contextHelp) ShortHelp() []key.Binding {
|
|||||||
b = append(b, k.Enter, k.Share, k.ShareMenu, k.Favourite, k.Open, k.Filter)
|
b = append(b, k.Enter, k.Share, k.ShareMenu, k.Favourite, k.Open, k.Filter)
|
||||||
default:
|
default:
|
||||||
reveal := k.Reveal
|
reveal := k.Reveal
|
||||||
if c.reveal != "" {
|
if c.revealed != nil {
|
||||||
reveal = key.NewBinding(key.WithKeys(k.Reveal.Keys()...), key.WithHelp(k.Reveal.Help().Key, c.reveal))
|
reveal = key.NewBinding(key.WithKeys(k.Reveal.Keys()...), key.WithHelp(k.Reveal.Help().Key, map[bool]string{false: "show", true: "hide"}[*c.revealed]))
|
||||||
}
|
}
|
||||||
b = append(b, reveal, k.Copy, k.Share, k.ShareMenu, k.Versions, k.Favourite, k.Filter)
|
b = append(b, reveal, k.Copy, k.Share, k.ShareMenu, k.Versions, k.Favourite, k.Filter)
|
||||||
}
|
}
|
||||||
@@ -258,7 +259,11 @@ func hint(b key.Binding, desc string) keyHint { return keyHint{keys.Name(b), des
|
|||||||
|
|
||||||
// move is "↑/↓ move" for an up and a down binding.
|
// move is "↑/↓ move" for an up and a down binding.
|
||||||
func move(up, down key.Binding) keyHint {
|
func move(up, down key.Binding) keyHint {
|
||||||
return keyHint{joinKeys(up, down), "move"}
|
u, d := keys.Name(up), keys.Name(down)
|
||||||
|
if u == "" || d == "" {
|
||||||
|
return keyHint{u + d, "move"}
|
||||||
|
}
|
||||||
|
return keyHint{u + "/" + d, "move"}
|
||||||
}
|
}
|
||||||
|
|
||||||
// withKey fills a hint's format with the binding's key ("· %s loads
|
// withKey fills a hint's format with the binding's key ("· %s loads
|
||||||
|
|||||||
+12
-1
@@ -199,7 +199,18 @@ func (m *Model) jumpTo(r ref.Ref, n store.Node) tea.Cmd {
|
|||||||
m.startNav(r, actGo, sourceRowKey(n.Account, n.Source))
|
m.startNav(r, actGo, sourceRowKey(n.Account, n.Source))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
choices := m.openWith(r.Kind, func(src provider.Source) bool { return src.ID == n.Source && n.Source != "" })
|
var choices []choice
|
||||||
|
for _, p := range m.providers {
|
||||||
|
if p.Kind() != r.Kind {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, src := range m.sourceResults[p.ID()].sources {
|
||||||
|
if src.ID == n.Source && n.Source != "" {
|
||||||
|
row := sourceRowKey(p.ID(), src.ID)
|
||||||
|
choices = append(choices, choice{label: "Open with " + m.sourceLabel(row), sourceRow: row})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
switch len(choices) {
|
switch len(choices) {
|
||||||
case 0:
|
case 0:
|
||||||
return m.openRef(r, actGo)
|
return m.openRef(r, actGo)
|
||||||
|
|||||||
+16
-20
@@ -8,6 +8,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"charm.land/bubbles/v2/textinput"
|
||||||
tea "charm.land/bubbletea/v2"
|
tea "charm.land/bubbletea/v2"
|
||||||
|
|
||||||
"git.kebcloud.no/keb/lazysecrets/internal/cachefile"
|
"git.kebcloud.no/keb/lazysecrets/internal/cachefile"
|
||||||
@@ -209,7 +210,18 @@ const defaultResolveTimeout = 30 * time.Second
|
|||||||
// openSource goes to a subscription or workspace, asking which account
|
// openSource goes to a subscription or workspace, asking which account
|
||||||
// when several list it. Sources are loaded already, so it needs no lookup.
|
// when several list it. Sources are loaded already, so it needs no lookup.
|
||||||
func (m *Model) openSource(r ref.Ref) {
|
func (m *Model) openSource(r ref.Ref) {
|
||||||
choices := m.openWith(r.Kind, func(src provider.Source) bool { return sourceMatches(r, src) })
|
var choices []choice
|
||||||
|
for _, p := range m.providers {
|
||||||
|
if p.Kind() != r.Kind {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, src := range m.sourceResults[p.ID()].sources {
|
||||||
|
if sourceMatches(r, src) {
|
||||||
|
row := sourceRowKey(p.ID(), src.ID)
|
||||||
|
choices = append(choices, choice{label: "Open with " + m.sourceLabel(row), sourceRow: row})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if row, ok := preferredRow(r, choices); ok {
|
if row, ok := preferredRow(r, choices); ok {
|
||||||
choices = []choice{{sourceRow: row}}
|
choices = []choice{{sourceRow: row}}
|
||||||
}
|
}
|
||||||
@@ -231,24 +243,6 @@ func (m *Model) openSource(r ref.Ref) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// openWith offers each loaded source of kind that matches, one per
|
|
||||||
// account that lists it.
|
|
||||||
func (m Model) openWith(kind provider.Kind, matches func(provider.Source) bool) []choice {
|
|
||||||
var choices []choice
|
|
||||||
for _, p := range m.providers {
|
|
||||||
if p.Kind() != kind {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
for _, src := range m.sourceResults[p.ID()].sources {
|
|
||||||
if matches(src) {
|
|
||||||
row := sourceRowKey(p.ID(), src.ID)
|
|
||||||
choices = append(choices, choice{label: "Open with " + m.sourceLabel(row), sourceRow: row})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return choices
|
|
||||||
}
|
|
||||||
|
|
||||||
// refLabel draws what a link points at like the favourites list: the
|
// refLabel draws what a link points at like the favourites list: the
|
||||||
// provider's icon and the path in its colour, " platform-prod /
|
// provider's icon and the path in its colour, " platform-prod /
|
||||||
// kv-app-prod / api-key", with the type and version after it. The
|
// kv-app-prod / api-key", with the type and version after it. The
|
||||||
@@ -837,8 +831,10 @@ func (m *Model) shareObject(o provider.Object, k string) tea.Cmd {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) startOpenPrompt() tea.Cmd {
|
func (m *Model) startOpenPrompt() tea.Cmd {
|
||||||
in := newTextInput("open: ", m.pathInputWidth())
|
in := textinput.New()
|
||||||
|
in.Prompt = "open: "
|
||||||
in.Placeholder = "lazysecrets:// link, Key Vault URL or reference"
|
in.Placeholder = "lazysecrets:// link, Key Vault URL or reference"
|
||||||
|
in.SetWidth(max(20, min(m.w-12, 64)))
|
||||||
m.prompt = &modal{
|
m.prompt = &modal{
|
||||||
title: "Open a link", input: &in,
|
title: "Open a link", input: &in,
|
||||||
lines: []string{m.st.Muted.Render("lazysecrets:// link, Key Vault URL or reference")},
|
lines: []string{m.st.Muted.Render("lazysecrets:// link, Key Vault URL or reference")},
|
||||||
|
|||||||
@@ -75,8 +75,10 @@ type modal struct {
|
|||||||
|
|
||||||
// showFilterMenu opens a menu with a filter input above the options.
|
// showFilterMenu opens a menu with a filter input above the options.
|
||||||
func (m *Model) showFilterMenu(title string, options []option) tea.Cmd {
|
func (m *Model) showFilterMenu(title string, options []option) tea.Cmd {
|
||||||
in := newTextInput("/", 40)
|
in := textinput.New()
|
||||||
|
in.Prompt = "/"
|
||||||
in.Placeholder = "type to filter"
|
in.Placeholder = "type to filter"
|
||||||
|
in.SetWidth(40)
|
||||||
m.prompt = &modal{title: title, options: options, all: options, filter: &in}
|
m.prompt = &modal{title: title, options: options, all: options, filter: &in}
|
||||||
m.prompt.move(0)
|
m.prompt.move(0)
|
||||||
m.flash = flash{}
|
m.flash = flash{}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package app
|
|||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"charm.land/bubbles/v2/textinput"
|
||||||
tea "charm.land/bubbletea/v2"
|
tea "charm.land/bubbletea/v2"
|
||||||
"github.com/charmbracelet/x/ansi"
|
"github.com/charmbracelet/x/ansi"
|
||||||
|
|
||||||
@@ -57,9 +58,11 @@ func (m *Model) editNote() tea.Cmd {
|
|||||||
c := o.Container
|
c := o.Container
|
||||||
key, name, path, src = objectKey(o), o.Name, []string{c.Source.Name, c.Name}, c.Source
|
key, name, path, src = objectKey(o), o.Name, []string{c.Source.Name, c.Name}, c.Source
|
||||||
}
|
}
|
||||||
in := newTextInput("note: ", 48)
|
in := textinput.New()
|
||||||
|
in.Prompt = "note: "
|
||||||
in.Placeholder = "e.g. prod SQL admin"
|
in.Placeholder = "e.g. prod SQL admin"
|
||||||
in.CharLimit = 80
|
in.CharLimit = 80
|
||||||
|
in.SetWidth(48)
|
||||||
in.SetValue(m.store.Node(key).Note)
|
in.SetValue(m.store.Node(key).Note)
|
||||||
in.CursorEnd()
|
in.CursorEnd()
|
||||||
m.prompt = &modal{
|
m.prompt = &modal{
|
||||||
|
|||||||
+15
-18
@@ -29,43 +29,41 @@ type persistTickMsg struct{}
|
|||||||
|
|
||||||
type persistedMsg struct{ err error }
|
type persistedMsg struct{ err error }
|
||||||
|
|
||||||
// keepers report whether a provider ID, or a cache key's, is an account
|
// keepCached reports whether a cache key's account still exists and
|
||||||
// that still exists and isn't a repo.
|
// isn't a repo: every key starts with the provider ID.
|
||||||
func (m *Model) keepers() (keep, keepKey func(string) bool) {
|
func (m *Model) keepCached(key string) bool { return m.keepProvider(cachefile.ProviderOf(key)) }
|
||||||
keep = cachefile.Keep(m.providers)
|
|
||||||
return keep, func(key string) bool { return keep(cachefile.ProviderOf(key)) }
|
func (m *Model) keepProvider(id string) bool { return cachefile.Keep(m.providers)(id) }
|
||||||
}
|
|
||||||
|
|
||||||
// loadCacheFile fills the cache from the file, leaving out what's too
|
// loadCacheFile fills the cache from the file, leaving out what's too
|
||||||
// old or belongs to accounts that aren't signed in any more.
|
// old or belongs to accounts that aren't signed in any more.
|
||||||
func (m *Model) loadCacheFile() {
|
func (m *Model) loadCacheFile() {
|
||||||
f := cachefile.Read(m.cachePath)
|
f := cachefile.Read(m.cachePath)
|
||||||
keep, keepKey := m.keepers()
|
|
||||||
now := m.now()
|
now := m.now()
|
||||||
recent := func(at time.Time) bool { return now.Sub(at) < cachefile.MaxAge }
|
recent := func(at time.Time) bool { return now.Sub(at) < cachefile.MaxAge }
|
||||||
for id, e := range f.Sources {
|
for id, e := range f.Sources {
|
||||||
if keep(id) && recent(e.At) {
|
if m.keepProvider(id) && recent(e.At) {
|
||||||
m.sourceResults[id] = sourcesMsg{gen: m.sourcesGen, provider: id, sources: e.Value}
|
m.sourceResults[id] = sourcesMsg{gen: m.sourcesGen, provider: id, sources: e.Value}
|
||||||
m.sourcesAt[id] = e.At
|
m.sourcesAt[id] = e.At
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for k, e := range f.Containers {
|
for k, e := range f.Containers {
|
||||||
if keepKey(k) && recent(e.At) {
|
if m.keepCached(k) && recent(e.At) {
|
||||||
m.cache.containers[k] = entry[[]provider.Container]{e.Value, e.At}
|
m.cache.containers[k] = entry[[]provider.Container]{e.Value, e.At}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for k, e := range f.Objects {
|
for k, e := range f.Objects {
|
||||||
if keepKey(k) && recent(e.At) {
|
if m.keepCached(k) && recent(e.At) {
|
||||||
m.cache.objects[k] = entry[[]provider.Object]{e.Value, e.At}
|
m.cache.objects[k] = entry[[]provider.Object]{e.Value, e.At}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for k, e := range f.Details {
|
for k, e := range f.Details {
|
||||||
if keepKey(k) && recent(e.At) {
|
if m.keepCached(k) && recent(e.At) {
|
||||||
m.cache.details[k] = entry[provider.Object]{e.Value, e.At}
|
m.cache.details[k] = entry[provider.Object]{e.Value, e.At}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for k, v := range f.Cursor {
|
for k, v := range f.Cursor {
|
||||||
if keepKey(k) {
|
if m.keepCached(k) {
|
||||||
m.cache.cursor[k] = v
|
m.cache.cursor[k] = v
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -74,30 +72,29 @@ func (m *Model) loadCacheFile() {
|
|||||||
// cacheSnapshot is the cache as the file holds it.
|
// cacheSnapshot is the cache as the file holds it.
|
||||||
func (m *Model) cacheSnapshot() cachefile.File {
|
func (m *Model) cacheSnapshot() cachefile.File {
|
||||||
f := cachefile.New()
|
f := cachefile.New()
|
||||||
keep, keepKey := m.keepers()
|
|
||||||
f.Dropped, m.cacheDropped = m.cacheDropped, nil
|
f.Dropped, m.cacheDropped = m.cacheDropped, nil
|
||||||
for id, r := range m.sourceResults {
|
for id, r := range m.sourceResults {
|
||||||
if r.err == nil && keep(id) {
|
if r.err == nil && m.keepProvider(id) {
|
||||||
f.Sources[id] = cachefile.Stamped[[]provider.Source]{At: m.sourcesAt[id], Value: r.sources}
|
f.Sources[id] = cachefile.Stamped[[]provider.Source]{At: m.sourcesAt[id], Value: r.sources}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for k, e := range m.cache.containers {
|
for k, e := range m.cache.containers {
|
||||||
if keepKey(k) {
|
if m.keepCached(k) {
|
||||||
f.Containers[k] = cachefile.Stamped[[]provider.Container]{At: e.at, Value: e.value}
|
f.Containers[k] = cachefile.Stamped[[]provider.Container]{At: e.at, Value: e.value}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for k, e := range m.cache.objects {
|
for k, e := range m.cache.objects {
|
||||||
if keepKey(k) {
|
if m.keepCached(k) {
|
||||||
f.Objects[k] = cachefile.Stamped[[]provider.Object]{At: e.at, Value: e.value}
|
f.Objects[k] = cachefile.Stamped[[]provider.Object]{At: e.at, Value: e.value}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for k, e := range m.cache.details {
|
for k, e := range m.cache.details {
|
||||||
if keepKey(k) {
|
if m.keepCached(k) {
|
||||||
f.Details[k] = cachefile.Stamped[provider.Object]{At: e.at, Value: e.value}
|
f.Details[k] = cachefile.Stamped[provider.Object]{At: e.at, Value: e.value}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for k, v := range m.cache.cursor {
|
for k, v := range m.cache.cursor {
|
||||||
if keepKey(k) {
|
if m.keepCached(k) {
|
||||||
f.Cursor[k] = v
|
f.Cursor[k] = v
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,22 +28,12 @@ func (m *Model) startRevealTimer() tea.Cmd {
|
|||||||
|
|
||||||
// revealTickCmd fires when the displayed second next changes.
|
// revealTickCmd fires when the displayed second next changes.
|
||||||
func (m *Model) revealTickCmd() tea.Cmd {
|
func (m *Model) revealTickCmd() tea.Cmd {
|
||||||
token := m.revealToken
|
wait := m.revealDeadline.Sub(m.now()) % time.Second
|
||||||
return tea.Tick(untilNextSecond(m.revealDeadline, m.now()), func(time.Time) tea.Msg { return revealTickMsg{token} })
|
if wait <= 0 {
|
||||||
}
|
wait = time.Second
|
||||||
|
|
||||||
// untilNextSecond is how long until a countdown to deadline shows its
|
|
||||||
// next whole second.
|
|
||||||
func untilNextSecond(deadline, now time.Time) time.Duration {
|
|
||||||
if wait := deadline.Sub(now) % time.Second; wait > 0 {
|
|
||||||
return wait
|
|
||||||
}
|
}
|
||||||
return time.Second
|
token := m.revealToken
|
||||||
}
|
return tea.Tick(wait, func(time.Time) tea.Msg { return revealTickMsg{token} })
|
||||||
|
|
||||||
// secondsLeft is a countdown to deadline, in whole seconds rounded up.
|
|
||||||
func secondsLeft(deadline, now time.Time) int {
|
|
||||||
return max(0, int(math.Ceil(deadline.Sub(now).Seconds())))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) revealTick(msg revealTickMsg) tea.Cmd {
|
func (m *Model) revealTick(msg revealTickMsg) tea.Cmd {
|
||||||
@@ -59,7 +49,8 @@ func (m *Model) revealTick(msg revealTickMsg) tea.Cmd {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) showHidesIn() {
|
func (m *Model) showHidesIn() {
|
||||||
m.detail.SetHidesIn(fmt.Sprintf("%ds", secondsLeft(m.revealDeadline, m.now())))
|
left := max(0, int(math.Ceil(m.revealDeadline.Sub(m.now()).Seconds())))
|
||||||
|
m.detail.SetHidesIn(fmt.Sprintf("%ds", left))
|
||||||
}
|
}
|
||||||
|
|
||||||
// hideValue hides the revealed value and stops its countdown.
|
// hideValue hides the revealed value and stops its countdown.
|
||||||
|
|||||||
@@ -341,19 +341,6 @@ func (m *Model) handleCrawl(msg crawlMsg) tea.Cmd {
|
|||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
// vaultsLoaded counts the vaults whose secrets are cached or can't be
|
|
||||||
// read, and says whether any are loading.
|
|
||||||
func (m *Model) vaultsLoaded(vaults []provider.Container) (loaded int, loading bool) {
|
|
||||||
for _, c := range vaults {
|
|
||||||
key := cachefile.ObjectsKey(c, provider.TypeSecret)
|
|
||||||
loading = loading || m.crawl.pending[key]
|
|
||||||
if _, ok := m.cache.objects[key]; ok || m.crawl.failed[key] {
|
|
||||||
loaded++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return loaded, loading
|
|
||||||
}
|
|
||||||
|
|
||||||
// sourceMark shows how much of a subscription or workspace is loaded:
|
// sourceMark shows how much of a subscription or workspace is loaded:
|
||||||
// ○ nothing, ◐ some vaults, ● every vault, ◌ loading, ✗ unreadable.
|
// ○ nothing, ◐ some vaults, ● every vault, ◌ loading, ✗ unreadable.
|
||||||
func (m *Model) sourceMark(row string) string {
|
func (m *Model) sourceMark(row string) string {
|
||||||
@@ -367,7 +354,14 @@ func (m *Model) sourceMark(row string) string {
|
|||||||
if !ok {
|
if !ok {
|
||||||
return m.st.Muted.Render("○")
|
return m.st.Muted.Render("○")
|
||||||
}
|
}
|
||||||
loaded, loading := m.vaultsLoaded(e.value)
|
loaded, loading := 0, false
|
||||||
|
for _, c := range e.value {
|
||||||
|
key := cachefile.ObjectsKey(c, provider.TypeSecret)
|
||||||
|
loading = loading || m.crawl.pending[key]
|
||||||
|
if _, ok := m.cache.objects[key]; ok || m.crawl.failed[key] {
|
||||||
|
loaded++
|
||||||
|
}
|
||||||
|
}
|
||||||
switch {
|
switch {
|
||||||
case loading:
|
case loading:
|
||||||
return m.st.Muted.Render("◌")
|
return m.st.Muted.Render("◌")
|
||||||
|
|||||||
@@ -95,7 +95,9 @@ func (m *Model) showFavourites() tea.Cmd { return m.openGoTo(tabFavs, false) }
|
|||||||
func (m *Model) showRecent() tea.Cmd { return m.openGoTo(tabRecent, false) }
|
func (m *Model) showRecent() tea.Cmd { return m.openGoTo(tabRecent, false) }
|
||||||
|
|
||||||
func (m *Model) openGoTo(tab int, typing bool) tea.Cmd {
|
func (m *Model) openGoTo(tab int, typing bool) tea.Cmd {
|
||||||
in := newTextInput("/", searchWidth(m.w)-6) // as wide as the popup
|
in := textinput.New()
|
||||||
|
in.Prompt = "/"
|
||||||
|
in.SetWidth(searchWidth(m.w) - 6) // as wide as the popup
|
||||||
v := &searchView{input: in, tab: tab, results: true}
|
v := &searchView{input: in, tab: tab, results: true}
|
||||||
v.favs, v.recent = m.storeEntries()
|
v.favs, v.recent = m.storeEntries()
|
||||||
m.search = v
|
m.search = v
|
||||||
@@ -614,7 +616,13 @@ func (m Model) loadHint() string {
|
|||||||
}
|
}
|
||||||
return m.st.Warning.Render("⚠ not loaded" + withKey(m.loadKey(), " · %s loads "+name))
|
return m.st.Warning.Render("⚠ not loaded" + withKey(m.loadKey(), " · %s loads "+name))
|
||||||
}
|
}
|
||||||
loaded, _ := m.vaultsLoaded(e.value)
|
loaded := 0
|
||||||
|
for _, c := range e.value {
|
||||||
|
key := cachefile.ObjectsKey(c, provider.TypeSecret)
|
||||||
|
if _, ok := m.cache.objects[key]; ok || m.crawl.failed[key] {
|
||||||
|
loaded++
|
||||||
|
}
|
||||||
|
}
|
||||||
if loaded == len(e.value) {
|
if loaded == len(e.value) {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
@@ -762,7 +770,7 @@ func (m Model) searchViewBox() string {
|
|||||||
body = append(body, " "+m.st.Error.Render(l))
|
body = append(body, " "+m.st.Error.Render(l))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
body = append(body, "", " "+panes.Tabs(m.st, ts, tabNames[:], m.search.tab))
|
body = append(body, "", " "+m.goToTabs(ts))
|
||||||
}
|
}
|
||||||
body = append(body, " "+v.input.View(), "")
|
body = append(body, " "+v.input.View(), "")
|
||||||
for _, l := range strings.Split(lists, "\n") {
|
for _, l := range strings.Split(lists, "\n") {
|
||||||
@@ -795,7 +803,7 @@ func (m Model) searchViewBox() string {
|
|||||||
// Tabs in the title, the current one underlined, like the panes; a
|
// Tabs in the title, the current one underlined, like the panes; a
|
||||||
// picker has its own title, and the tabs over the input.
|
// picker has its own title, and the tabs over the input.
|
||||||
if v.picker == nil {
|
if v.picker == nil {
|
||||||
head := ts.Render(goToTitle+" ") + panes.Tabs(m.st, ts, tabNames[:], m.search.tab)
|
head := ts.Render(goToTitle+" ") + m.goToTabs(ts)
|
||||||
return panes.Box(m.st, head, foot, body, w, len(body)+2, focused)
|
return panes.Box(m.st, head, foot, body, w, len(body)+2, focused)
|
||||||
}
|
}
|
||||||
// A picker: in its own border, under what it's for.
|
// A picker: in its own border, under what it's for.
|
||||||
@@ -848,3 +856,19 @@ func pickable(o option) (ref.Ref, bool) {
|
|||||||
}
|
}
|
||||||
return r, true
|
return r, true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// goToTabs draws Go to's tabs, the current one underlined.
|
||||||
|
func (m Model) goToTabs(ts lipgloss.Style) string {
|
||||||
|
var out string
|
||||||
|
for i, name := range tabNames {
|
||||||
|
if i > 0 {
|
||||||
|
out += ts.Render(" - ")
|
||||||
|
}
|
||||||
|
if i == m.search.tab {
|
||||||
|
out += ts.Underline(true).Render(name)
|
||||||
|
} else {
|
||||||
|
out += m.st.Muted.Render(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"charm.land/bubbles/v2/textinput"
|
||||||
tea "charm.land/bubbletea/v2"
|
tea "charm.land/bubbletea/v2"
|
||||||
"github.com/charmbracelet/x/ansi"
|
"github.com/charmbracelet/x/ansi"
|
||||||
|
|
||||||
@@ -337,7 +338,9 @@ func (m *Model) openFailed(s share.Sealed, err error, whole bool) {
|
|||||||
// askShareDir asks where to save files from a repo that isn't here. It
|
// askShareDir asks where to save files from a repo that isn't here. It
|
||||||
// stays until they're saved, saying why when they can't be.
|
// stays until they're saved, saying why when they can't be.
|
||||||
func (m *Model) askShareDir(msg shareOpenedMsg) tea.Cmd {
|
func (m *Model) askShareDir(msg shareOpenedMsg) tea.Cmd {
|
||||||
in := newTextInput("folder: ", m.pathInputWidth())
|
in := textinput.New()
|
||||||
|
in.Prompt = "folder: "
|
||||||
|
in.SetWidth(max(20, min(m.w-12, 64)))
|
||||||
dir := m.placeDir
|
dir := m.placeDir
|
||||||
if dir == "" {
|
if dir == "" {
|
||||||
dir, _ = os.Getwd()
|
dir, _ = os.Getwd()
|
||||||
|
|||||||
@@ -134,7 +134,10 @@ func (m *Model) rebuildSources() {
|
|||||||
}
|
}
|
||||||
for _, p := range m.providers {
|
for _, p := range m.providers {
|
||||||
kind, id := p.Kind(), p.ID()
|
kind, id := p.Kind(), p.ID()
|
||||||
account := accountOf(provider.Source{Kind: kind, ProviderLabel: p.Label()})
|
account, _ := strings.CutPrefix(p.Label(), string(kind)+" · ")
|
||||||
|
if account == p.Label() {
|
||||||
|
account = ""
|
||||||
|
}
|
||||||
r, done := m.sourceResults[id]
|
r, done := m.sourceResults[id]
|
||||||
sources := r.sources
|
sources := r.sources
|
||||||
if !flat {
|
if !flat {
|
||||||
@@ -212,6 +215,17 @@ func (m Model) currentSource() (provider.Source, bool) {
|
|||||||
return provider.Source{}, false
|
return provider.Source{}, false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// starredSourcesFirst moves starred sources to the front, keeping the
|
||||||
|
// order otherwise. It doesn't modify sources.
|
||||||
|
func (m Model) starredSourcesFirst(sources []provider.Source) []provider.Source {
|
||||||
|
out := slices.Clone(sources)
|
||||||
|
rank := func(s provider.Source) int {
|
||||||
|
return boolRank(m.store.Node(sourceKey(s)).StarredFor(s.Provider))
|
||||||
|
}
|
||||||
|
slices.SortStableFunc(out, func(a, b provider.Source) int { return rank(a) - rank(b) })
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
// starRank orders the Favourites row, then starred sources, then the rest.
|
// starRank orders the Favourites row, then starred sources, then the rest.
|
||||||
func starRank(mem *store.Store, r sourceRow) int {
|
func starRank(mem *store.Store, r sourceRow) int {
|
||||||
switch {
|
switch {
|
||||||
@@ -225,6 +239,13 @@ func starRank(mem *store.Store, r sourceRow) int {
|
|||||||
return 3
|
return 3
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func boolRank(starred bool) int {
|
||||||
|
if starred {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
// selectedSource is the source under the cursor; false on a header.
|
// selectedSource is the source under the cursor; false on a header.
|
||||||
func (m Model) selectedSource() (provider.Source, bool) {
|
func (m Model) selectedSource() (provider.Source, bool) {
|
||||||
r, ok := m.sources.Selected()
|
r, ok := m.sources.Selected()
|
||||||
|
|||||||
@@ -2,11 +2,13 @@ package app
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
tea "charm.land/bubbletea/v2"
|
tea "charm.land/bubbletea/v2"
|
||||||
|
|
||||||
|
"git.kebcloud.no/keb/lazysecrets/internal/browser"
|
||||||
"git.kebcloud.no/keb/lazysecrets/internal/store"
|
"git.kebcloud.no/keb/lazysecrets/internal/store"
|
||||||
"git.kebcloud.no/keb/lazysecrets/internal/update"
|
"git.kebcloud.no/keb/lazysecrets/internal/update"
|
||||||
)
|
)
|
||||||
@@ -128,7 +130,15 @@ func (m *Model) showUpdate() {
|
|||||||
run: func(m *Model) tea.Cmd { return m.install(r) }}}
|
run: func(m *Model) tea.Cmd { return m.install(r) }}}
|
||||||
if r.URL != "" {
|
if r.URL != "" {
|
||||||
opts = append(opts, option{key: "o", label: "Open the release page", run: func(m *Model) tea.Cmd {
|
opts = append(opts, option{key: "o", label: "Open the release page", run: func(m *Model) tea.Cmd {
|
||||||
return m.openPage("the release page", r.URL, func() {})
|
switch err := m.openURL(r.URL); {
|
||||||
|
case errors.Is(err, browser.ErrRemote):
|
||||||
|
cmd := m.copyText("link to the release", r.URL, false)
|
||||||
|
m.flash.text += " (can't open a browser over SSH)"
|
||||||
|
return cmd
|
||||||
|
case err != nil:
|
||||||
|
m.flash = flash{text: "open: " + err.Error(), isErr: true}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}})
|
}})
|
||||||
}
|
}
|
||||||
m.showMenu("lazysecrets "+r.Tag, lines, opts)
|
m.showMenu("lazysecrets "+r.Tag, lines, opts)
|
||||||
|
|||||||
@@ -69,20 +69,3 @@ func TitleStyle(st styles.Styles, focused bool) lipgloss.Style {
|
|||||||
}
|
}
|
||||||
return st.TitleBlurred
|
return st.TitleBlurred
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tabs draws tab names for a title in style ts, the current one
|
|
||||||
// underlined and the rest muted.
|
|
||||||
func Tabs(st styles.Styles, ts lipgloss.Style, names []string, current int) string {
|
|
||||||
var out string
|
|
||||||
for i, name := range names {
|
|
||||||
if i > 0 {
|
|
||||||
out += ts.Render(" - ")
|
|
||||||
}
|
|
||||||
if i == current {
|
|
||||||
out += ts.Underline(true).Render(name)
|
|
||||||
} else {
|
|
||||||
out += st.Muted.Render(name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
+21
-27
@@ -188,31 +188,20 @@ func carryStyle(lines []string) []string {
|
|||||||
return lines
|
return lines
|
||||||
}
|
}
|
||||||
|
|
||||||
var valueIndent = strings.Repeat(" ", 12)
|
|
||||||
|
|
||||||
// fieldLabel starts a detail line: the label, padded so values line up
|
|
||||||
// at valueIndent.
|
|
||||||
func fieldLabel(st styles.Styles, label string) string {
|
|
||||||
return " " + st.Label.Render(fmt.Sprintf("%-10s", label)) + " "
|
|
||||||
}
|
|
||||||
|
|
||||||
// lines renders the body, wrapped to the pane width.
|
// lines renders the body, wrapped to the pane width.
|
||||||
func (d Detail) lines(st styles.Styles) []string {
|
func (d Detail) lines(st styles.Styles) []string {
|
||||||
inner := max(1, d.w-3)
|
inner := max(1, d.w-3)
|
||||||
var out []string
|
var out []string
|
||||||
field := func(label, value string) {
|
field := func(label, value string) {
|
||||||
|
prefix := " " + st.Label.Render(fmt.Sprintf("%-10s", label)) + " "
|
||||||
|
indent := strings.Repeat(" ", 12)
|
||||||
wrapped := carryStyle(strings.Split(ansi.Hardwrap(value, max(1, inner-11), true), "\n"))
|
wrapped := carryStyle(strings.Split(ansi.Hardwrap(value, max(1, inner-11), true), "\n"))
|
||||||
out = append(out, fieldLabel(st, label)+wrapped[0])
|
out = append(out, prefix+wrapped[0])
|
||||||
for _, w := range wrapped[1:] {
|
for _, w := range wrapped[1:] {
|
||||||
out = append(out, valueIndent+w)
|
out = append(out, indent+w)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
muted := st.Muted.Render
|
muted := st.Muted.Render
|
||||||
note := func() {
|
|
||||||
if d.note != "" {
|
|
||||||
field("note", st.Favourite.Render(d.note))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if d.container == nil {
|
if d.container == nil {
|
||||||
return []string{muted(" Select a vault or scope to browse its secrets.")}
|
return []string{muted(" Select a vault or scope to browse its secrets.")}
|
||||||
@@ -222,27 +211,30 @@ func (d Detail) lines(st styles.Styles) []string {
|
|||||||
if containerLabel == "" {
|
if containerLabel == "" {
|
||||||
containerLabel = "vault"
|
containerLabel = "vault"
|
||||||
}
|
}
|
||||||
source := d.Icons.Prefix(c.Source.Kind) + cmp.Or(c.Source.ProviderLabel, string(c.Source.Kind)) + " / " + c.Source.Name
|
|
||||||
|
|
||||||
if d.secret == nil {
|
if d.secret == nil {
|
||||||
field(containerLabel, c.Name)
|
field(containerLabel, c.Name)
|
||||||
note()
|
if d.note != "" {
|
||||||
|
field("note", st.Favourite.Render(d.note))
|
||||||
|
}
|
||||||
if c.Detail != "" {
|
if c.Detail != "" {
|
||||||
field("", muted(c.Detail))
|
field("", muted(c.Detail))
|
||||||
}
|
}
|
||||||
field("source", source)
|
field("source", d.Icons.Prefix(c.Source.Kind)+cmp.Or(c.Source.ProviderLabel, string(c.Source.Kind))+" / "+c.Source.Name)
|
||||||
out = append(out, "", muted(" Select a secret to see its details."))
|
out = append(out, "", muted(" Select a secret to see its details."))
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
s := *d.secret
|
s := *d.secret
|
||||||
field("name", s.Name)
|
field("name", s.Name)
|
||||||
note()
|
if d.note != "" {
|
||||||
|
field("note", st.Favourite.Render(d.note))
|
||||||
|
}
|
||||||
field(containerLabel, c.Name)
|
field(containerLabel, c.Name)
|
||||||
if c.Source.Kind != provider.KindAzure && c.Detail != "" {
|
if c.Source.Kind != provider.KindAzure && c.Detail != "" {
|
||||||
field("", muted(c.Detail))
|
field("", muted(c.Detail))
|
||||||
}
|
}
|
||||||
field("source", source)
|
field("source", d.Icons.Prefix(c.Source.Kind)+cmp.Or(c.Source.ProviderLabel, string(c.Source.Kind))+" / "+c.Source.Name)
|
||||||
if s.Version != "" {
|
if s.Version != "" {
|
||||||
field("version", s.Version)
|
field("version", s.Version)
|
||||||
}
|
}
|
||||||
@@ -306,12 +298,14 @@ func (d Detail) lines(st styles.Styles) []string {
|
|||||||
case provider.TypeCertificate:
|
case provider.TypeCertificate:
|
||||||
label, hidden, reveal = "cert", "", "show the certificate"
|
label, hidden, reveal = "cert", "", "show the certificate"
|
||||||
}
|
}
|
||||||
|
indent := strings.Repeat(" ", 12)
|
||||||
|
|
||||||
out = append(out, "")
|
out = append(out, "")
|
||||||
switch d.state {
|
switch d.state {
|
||||||
case ValueHidden:
|
case ValueHidden:
|
||||||
if hidden != "" {
|
if hidden != "" {
|
||||||
field(label, hidden)
|
field(label, hidden)
|
||||||
out = append(out, valueIndent+hints(reveal))
|
out = append(out, indent+hints(reveal))
|
||||||
} else {
|
} else {
|
||||||
field(label, hints(reveal))
|
field(label, hints(reveal))
|
||||||
}
|
}
|
||||||
@@ -328,9 +322,9 @@ func (d Detail) lines(st styles.Styles) []string {
|
|||||||
for j, w := range strings.Split(ansi.Wrap(text, max(1, inner-11-len(lead)), ""), "\n") {
|
for j, w := range strings.Split(ansi.Wrap(text, max(1, inner-11-len(lead)), ""), "\n") {
|
||||||
w = lead + w
|
w = lead + w
|
||||||
if i == 0 && j == 0 {
|
if i == 0 && j == 0 {
|
||||||
out = append(out, fieldLabel(st, label)+st.Error.Render(w))
|
out = append(out, " "+st.Label.Render(fmt.Sprintf("%-10s", label))+" "+st.Error.Render(w))
|
||||||
} else {
|
} else {
|
||||||
out = append(out, valueIndent+st.Error.Render(w))
|
out = append(out, strings.Repeat(" ", 12)+st.Error.Render(w))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -341,10 +335,10 @@ func (d Detail) lines(st styles.Styles) []string {
|
|||||||
for _, line := range lines {
|
for _, line := range lines {
|
||||||
for _, w := range strings.Split(ansi.Hardwrap(line, max(1, inner-11), true), "\n") {
|
for _, w := range strings.Split(ansi.Hardwrap(line, max(1, inner-11), true), "\n") {
|
||||||
if first {
|
if first {
|
||||||
out = append(out, fieldLabel(st, label)+st.Value.Render(w))
|
out = append(out, " "+st.Label.Render(fmt.Sprintf("%-10s", label))+" "+st.Value.Render(w))
|
||||||
first = false
|
first = false
|
||||||
} else {
|
} else {
|
||||||
out = append(out, valueIndent+st.Value.Render(w))
|
out = append(out, indent+st.Value.Render(w))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -358,7 +352,7 @@ func (d Detail) lines(st styles.Styles) []string {
|
|||||||
if d.hidesIn != "" {
|
if d.hidesIn != "" {
|
||||||
more = append(more, "hides in "+d.hidesIn)
|
more = append(more, "hides in "+d.hidesIn)
|
||||||
}
|
}
|
||||||
out = append(out, valueIndent+hints("hide", more...))
|
out = append(out, indent+hints("hide", more...))
|
||||||
}
|
}
|
||||||
return append(out, d.versionLines(st)...)
|
return append(out, d.versionLines(st)...)
|
||||||
}
|
}
|
||||||
@@ -379,7 +373,7 @@ func (d Detail) versionLines(st styles.Styles) []string {
|
|||||||
head += muted(" · " + d.VersionsKey + " to browse")
|
head += muted(" · " + d.VersionsKey + " to browse")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
out := []string{"", fieldLabel(st, "versions") + head}
|
out := []string{"", " " + st.Label.Render(fmt.Sprintf("%-10s", "versions")) + " " + head}
|
||||||
current := d.versions[0].Version
|
current := d.versions[0].Version
|
||||||
for i, v := range d.versions {
|
for i, v := range d.versions {
|
||||||
if i == maxVersionLines {
|
if i == maxVersionLines {
|
||||||
|
|||||||
@@ -329,7 +329,17 @@ func (l List[T]) View(st styles.Styles, focused bool, spin string) string {
|
|||||||
ts := TitleStyle(st, focused)
|
ts := TitleStyle(st, focused)
|
||||||
title := ts.Render(l.Title)
|
title := ts.Render(l.Title)
|
||||||
if len(l.Tabs) > 1 {
|
if len(l.Tabs) > 1 {
|
||||||
title += ts.Render(" ") + Tabs(st, ts, l.Tabs, l.Tab)
|
for i, tab := range l.Tabs {
|
||||||
|
sep := " - "
|
||||||
|
if i == 0 {
|
||||||
|
sep = " "
|
||||||
|
}
|
||||||
|
if i == l.Tab {
|
||||||
|
title += ts.Render(sep) + ts.Underline(true).Render(tab)
|
||||||
|
} else {
|
||||||
|
title += ts.Render(sep) + st.Muted.Render(tab)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if q := l.input.Value(); q != "" && !l.filtering {
|
if q := l.input.Value(); q != "" && !l.filtering {
|
||||||
title += ts.Render(" /" + q)
|
title += ts.Render(" /" + q)
|
||||||
@@ -360,13 +370,26 @@ func (l List[T]) View(st styles.Styles, focused bool, spin string) string {
|
|||||||
default:
|
default:
|
||||||
for i := l.offset; i < len(l.visible) && i < l.offset+rows; i++ {
|
for i := l.offset; i < len(l.visible) && i < l.offset+rows; i++ {
|
||||||
line := l.item.Render(st, l.items[l.visible[i]], inner-1)
|
line := l.item.Render(st, l.items[l.visible[i]], inner-1)
|
||||||
line = " " + MarkMatches(line, l.highlight(i, ansi.Strip(line)), st.Match)
|
plain := ansi.Strip(line)
|
||||||
|
hits := l.highlight(i, plain)
|
||||||
if i == l.cursor {
|
if i == l.cursor {
|
||||||
base := st.SelectedBlurred
|
base := st.SelectedBlurred
|
||||||
if focused {
|
if focused {
|
||||||
base = st.Selected
|
base = st.Selected
|
||||||
}
|
}
|
||||||
line = Highlight(ansi.Truncate(line, inner, "…"), base, inner)
|
text := " " + line
|
||||||
|
if len(hits) > 0 {
|
||||||
|
for j := range hits {
|
||||||
|
hits[j]++ // leading space
|
||||||
|
}
|
||||||
|
text = lipgloss.StyleRanges(text, matchRanges(" "+plain, hits, st.Match)...)
|
||||||
|
}
|
||||||
|
line = Highlight(ansi.Truncate(text, inner, "…"), base, inner)
|
||||||
|
} else if len(hits) > 0 {
|
||||||
|
// Style just the matches, keeping the row's own colours.
|
||||||
|
line = " " + lipgloss.StyleRanges(line, matchRanges(plain, hits, st.Match)...)
|
||||||
|
} else {
|
||||||
|
line = " " + line
|
||||||
}
|
}
|
||||||
body = append(body, line)
|
body = append(body, line)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user