Hello world
Release / Build and publish (push) Successful in 22s

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
kebandClaude Opus 5.5 committed 2026-10-12 11:35:25 +02:00
commit 45fd80b418
130 files changed
+26739

No files matched your search

+245
View File
@@ -0,0 +1,245 @@
package app
import (
"slices"
"strings"
tea "charm.land/bubbletea/v2"
"git.kebcloud.no/keb/lazysecrets/internal/provider"
"git.kebcloud.no/keb/lazysecrets/internal/ref"
"git.kebcloud.no/keb/lazysecrets/internal/store"
"git.kebcloud.no/keb/lazysecrets/internal/ui/styles"
)
// What lazysecrets remembers about subscriptions, vaults and secrets —
// stars, notes — lives in the store, one record per item keyed by its
// lazysecrets:// link. Keys leave out the account, so a star or note on a
// subscription shows under every account that sees it.
const star = "★ "
func sourceKey(s provider.Source) string {
if r, err := ref.ForSource(s); err == nil {
return r.String()
}
return "source:" + string(s.Kind) + "/" + s.ID
}
func storeKey(c provider.Container) string {
if r, err := ref.ForContainer(c, provider.TypeSecret); err == nil {
return r.String()
}
return "container:" + c.ID
}
func objectKey(o provider.Object) string {
if r, err := ref.ForObject(o, ""); err == nil {
return r.String()
}
return "object:" + o.ID
}
// remember records context with a change, so lists of remembered items
// can show and jump to them later.
func (m *Model) remember(key, name string, path []string, src provider.Source, change func(*store.Node)) error {
ctx := withContext(name, path, src)
return m.store.Update(key, func(n *store.Node) {
ctx(n)
change(n)
})
}
// withContext sets a record's context: where the item is, and the account
// that read it. A vault opened from a link has no known subscription;
// that keeps the one already recorded.
func withContext(name string, path []string, src provider.Source) func(*store.Node) {
return func(n *store.Node) {
n.Name = name
if src.ID != "" || n.Source == "" {
n.Path, n.Source = path, src.ID
}
if src.Provider != "" {
n.Account = src.Provider
}
}
}
// refreshContext updates a remembered item's context after reaching it,
// e.g. the account that could read it. Items without a record are left
// alone, so browsing doesn't write the file.
func (m *Model) refreshContext(key, name string, path []string, src provider.Source) {
if m.store.Has(key) && src.Provider != "" {
_ = m.remember(key, name, path, src, func(*store.Node) {})
}
}
// containerRender draws a vault or scope, starred or not. In the
// Opened links list it also names the source, since they come from many.
func (m Model) containerRender(withSource bool) func(styles.Styles, provider.Container, int) string {
st, iconSet := m.store, m.icons
return func(sty styles.Styles, c provider.Container, width int) string {
n := st.Node(storeKey(c))
line := c.Name
if c.Source.Kind == provider.KindRepo {
// A secret file: a lock for an encrypted one, a file in the
// warning colour for plain text, and its type after it
// ("sops · age", "env · plain text").
icon, style := iconSet.Encrypted, sty.KindRepo
if !strings.HasPrefix(c.Detail, "sops") {
icon, style = iconSet.PlainFile, sty.Warning
}
if icon != "" {
line = style.Render(icon) + " " + line
}
line += sty.Muted.Render(" " + c.Detail)
}
if n.StarredFor(c.Source.Provider) {
line = sty.Favourite.Render(star) + line
}
if withSource {
src := c.Source.Name
if account, ok := strings.CutPrefix(c.Source.ProviderLabel, string(c.Source.Kind)+" · "); ok {
src = account + " / " + src
}
line += sty.Muted.Render(" · " + src)
}
return withNote(sty, line, n.Note, width)
}
}
func (m Model) objectRender() func(styles.Styles, provider.Object, int) string {
st := m.store
return func(sty styles.Styles, o provider.Object, width int) string {
n := st.Node(objectKey(o))
line := o.Name
if o.Enabled != nil && !*o.Enabled {
line = sty.Muted.Render(o.Name + " (disabled)")
}
if n.StarredFor(o.Container.Source.Provider) {
line = sty.Favourite.Render(star) + line
}
switch state, left := m.expiry(o); state {
case expired:
line += " " + sty.Error.Render("✗ expired")
case expiresSoon:
line += " " + sty.Warning.Render("⚠ "+daysLeft(left))
}
return withNote(sty, line, n.Note, width)
}
}
// starredContainersFirst moves starred vaults and scopes to the front,
// keeping the order otherwise. It doesn't modify items, which may be cached.
func (m Model) starredContainersFirst(items []provider.Container) []provider.Container {
out := slices.Clone(items)
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
}
// starredFirst moves starred objects to the front, keeping the order
// otherwise. It doesn't modify items, which may be cached.
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))
})
return out
}
// showLinkedContainers lists the vaults opened from links.
func (m *Model) showLinkedContainers() {
m.containers.Title, m.containers.Empty = "[2] Opened links", "no links opened"
m.containers.SetRender(m.containerRender(true))
m.containers.SetItems(m.linked)
m.containers.Select(m.cache.cursor[linksRowID])
}
// rebind records the account that served a vault opened from a link. The
// container's key includes the account, so the selection moves to the
// new key without reloading. It returns the new key for the object list.
func (m *Model) rebind(c provider.Container) string {
m.refreshContext(storeKey(c), c.Name, []string{c.Source.Name}, c.Source)
for i := range m.linked {
if m.linked[i].ID == c.ID {
m.linked[i].Source = c.Source
}
}
key := containerKey(c) + "|" + string(m.objType)
if m.onLinks() {
m.containers.SetItems(m.linked)
m.containers.Select(containerKey(c))
m.selContainer = key
}
return key
}
// resortStarred puts starred items first again in every pane, after
// stars changed elsewhere (the favourites list).
func (m *Model) resortStarred() {
m.rebuildSources()
if !m.onLinks() {
m.containers.SetItems(m.starredContainersFirst(m.containers.Items()))
}
if m.versionsOf == nil {
m.secrets.SetItems(m.starredFirst(m.secrets.Items()))
}
}
// toggleFavourite stars or unstars the selected subscription (Sources),
// vault or scope (Vaults) or object (Secrets and detail panes).
func (m *Model) toggleFavourite() tea.Cmd {
var name string
var on bool
// A star is the item through the account showing it.
var err error
switch m.focus {
case paneSources:
src, ok := m.selectedSource()
if !ok {
m.flash = warning("select a subscription or workspace to star")
return nil
}
name = src.Name
toggle := func(n *store.Node) { on = n.ToggleStar(src.Provider) }
if err = m.remember(sourceKey(src), src.Name, nil, src, toggle); err == nil {
m.rebuildSources() // re-sorts; the cursor follows the source
}
case paneContainers:
c, ok := m.containers.Selected()
if !ok {
return nil
}
name = c.Name
toggle := func(n *store.Node) { on = n.ToggleStar(c.Source.Provider) }
if err = m.remember(storeKey(c), c.Name, []string{c.Source.Name}, c.Source, toggle); err == nil {
m.containers.SetItems(m.starredContainersFirst(m.containers.Items()))
}
default:
if m.versionsOf != nil {
m.flash = warning("star the item from the list rather than a version")
return nil
}
o, ok := m.detail.Secret()
if !ok {
return nil
}
name = o.Name
c := o.Container
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 {
m.secrets.SetItems(m.starredFirst(m.secrets.Items()))
}
}
switch {
case err != nil:
m.flash = flash{text: "saving: " + err.Error(), isErr: true}
case on:
m.flash = done("starred " + name)
default:
m.flash = done("unstarred " + name)
}
return nil
}