From 759c7d68630f5fd8473679ff28d087d0d1c223ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karl-Einar=20Bergstr=C3=B8m?= Date: Thu, 15 Oct 2026 14:30:23 +0200 Subject: [PATCH] feat: Go to shows a repo file's icon before its name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Search, Favourites, Recent and Opened links show the file's icon in its colour (lock, leaf, whale, plain file), as the files pane does: saft / 󰈔 backend/.env / COMPOSE_PROJECT_NAME. --- internal/ui/app/favourites.go | 41 ++++++++++++++++++++++------------- internal/ui/app/jump.go | 34 ++++++++++++++++++++++------- internal/ui/app/links.go | 2 +- internal/ui/app/search.go | 10 ++++----- 4 files changed, 58 insertions(+), 29 deletions(-) diff --git a/internal/ui/app/favourites.go b/internal/ui/app/favourites.go index f5a46b7..3804d54 100644 --- a/internal/ui/app/favourites.go +++ b/internal/ui/app/favourites.go @@ -97,26 +97,15 @@ func (m *Model) refreshContext(key, name string, path []string, src provider.Sou // 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 + st := m.store 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 leaf for - // Spring config, a whale for compose, a file in the warning - // colour for plain text, and its type after it ("sops · + // A secret file: its icon, and its type after it ("sops · // age", "env · plain text"). - icon, style := iconSet.Encrypted, sty.KindRepo - switch { - case strings.HasPrefix(c.Detail, "spring"): - icon, style = iconSet.Spring, sty.Spring - case strings.HasPrefix(c.Detail, "compose"): - icon, style = iconSet.Compose, sty.Compose - case !strings.HasPrefix(c.Detail, "sops"): - icon, style = iconSet.PlainFile, sty.Warning - } - if icon != "" { - line = style.Render(icon) + " " + line + if icon := m.fileIcon(sty, c); icon != "" { + line = icon + " " + line } line += sty.Muted.Render(" " + c.Detail) } @@ -155,6 +144,28 @@ func (m Model) objectRender() func(styles.Styles, provider.Object, int) string { } } +// fileIcon is a repo file's icon, in its colour: a lock for an encrypted +// one, a leaf for Spring config, a whale for compose, a file in the +// warning colour for plain text; "" for other vaults, or without icons. +func (m Model) fileIcon(sty styles.Styles, c provider.Container) string { + if c.Source.Kind != provider.KindRepo { + return "" + } + icon, style := m.icons.Encrypted, sty.KindRepo + switch { + case strings.HasPrefix(c.Detail, "spring"): + icon, style = m.icons.Spring, sty.Spring + case strings.HasPrefix(c.Detail, "compose"): + icon, style = m.icons.Compose, sty.Compose + case !strings.HasPrefix(c.Detail, "sops"): + icon, style = m.icons.PlainFile, sty.Warning + } + if icon == "" { + return "" + } + return style.Render(icon) +} + // dimIf draws a hidden item's row (shown with H) in the muted colour, // whole, so it stands apart from what's listed by the provider's rules. func dimIf(sty styles.Styles, hidden bool, line string) string { diff --git a/internal/ui/app/jump.go b/internal/ui/app/jump.go index 78c0352..2e3c28c 100644 --- a/internal/ui/app/jump.go +++ b/internal/ui/app/jump.go @@ -47,7 +47,7 @@ func (m *Model) storeEntries() (favs, recent []option) { account = strings.Trim(detail+" · "+account, " ·") sort := strings.ToLower(strings.Join(path, "/")) + " " + account out = append(out, entry{depth, sort, option{ - label: m.pathLabel(r.Kind, path, account), note: n.Note, mark: " ", + label: m.pathLabel(r.Kind, path, account, m.refFileIcon(r)), note: n.Note, mark: " ", id: prefix + key + "|" + acct, star: key, starAccount: acct, starred: n.StarredFor(acct), plain: m.inPlainEnv(r), run: func(m *Model) tea.Cmd { @@ -128,12 +128,15 @@ const recentMax = 20 // pathLabel draws a jump list entry: the provider's icon and the path in // its colour, then the account. -func (m Model) pathLabel(kind provider.Kind, path []string, account string) string { +func (m Model) pathLabel(kind provider.Kind, path []string, account, fileIcon string) string { style := m.st.Kind(kind) parts := make([]string, len(path)) for i, p := range path { parts[i] = style.Render(p) } + if fileIcon != "" && len(parts) > 1 { + parts[1] = fileIcon + " " + parts[1] + } label := style.Render(m.icons.Prefix(kind)) + strings.Join(parts, m.st.Muted.Render(" / ")) if account != "" { label += m.st.Muted.Render(" · " + account) @@ -144,19 +147,34 @@ func (m Model) pathLabel(kind provider.Kind, path []string, account string) stri // inPlainEnv reports whether r is a key in a plain env file, as far as // the files loaded tell. func (m Model) inPlainEnv(r ref.Ref) bool { - if r.Kind != provider.KindRepo || r.Name == "" { - return false + c, ok := m.repoFile(r) + return ok && r.Name != "" && c.Detail == provider.DetailPlainEnv +} + +// repoFile is the repo file r is or is in, as far as the files loaded +// tell. +func (m Model) repoFile(r ref.Ref) (provider.Container, bool) { + if r.Kind != provider.KindRepo || r.IsSource() { + return provider.Container{}, false } file := r file.Name, file.Type = "", "" for _, e := range m.cache.containers { for _, c := range e.value { - if c.Detail == provider.DetailPlainEnv && storeKey(c) == file.String() { - return true + if storeKey(c) == file.String() { + return c, true } } } - return false + return provider.Container{}, false +} + +// refFileIcon is the icon of the repo file r is or is in, if it's loaded. +func (m Model) refFileIcon(r ref.Ref) string { + if c, ok := m.repoFile(r); ok { + return m.fileIcon(m.st, c) + } + return "" } // objectDetail is the Detail of the secret with the store key key, as far @@ -175,7 +193,7 @@ func (m Model) objectDetail(key string) string { // repoLabel draws a repo, or a file in it, as everywhere else: the repo // icon and colour, "app / apps/api/.env". func (m Model) repoLabel(repo string, file ...string) string { - return m.pathLabel(provider.KindRepo, append([]string{repo}, file...), "") + return m.pathLabel(provider.KindRepo, append([]string{repo}, file...), "", "") } // sourceMatches reports whether src is the subscription or workspace r diff --git a/internal/ui/app/links.go b/internal/ui/app/links.go index 51748be..5bc4c57 100644 --- a/internal/ui/app/links.go +++ b/internal/ui/app/links.go @@ -269,7 +269,7 @@ func (m Model) refLabel(r ref.Ref) string { default: path = append(path, r.ContainerName(), r.Name) } - label := m.pathLabel(r.Kind, path, "") + label := m.pathLabel(r.Kind, path, "", m.refFileIcon(r)) var extra []string if t := strings.ToLower(pluralLabels[r.Type]); t != "" && r.Type != provider.TypeSecret { if r.IsContainer() { diff --git a/internal/ui/app/search.go b/internal/ui/app/search.go index 977948d..c08dae1 100644 --- a/internal/ui/app/search.go +++ b/internal/ui/app/search.go @@ -58,11 +58,11 @@ func (m *Model) searchEntries() (sources, items []option) { var es []entry // key is the store key, so ctrl+f stars the entry; mark shows how // much is loaded, reload (ctrl+r) loads it again. - add := func(depth int, row string, kind provider.Kind, path []string, account, key, mark string, run, reload func(*Model) tea.Cmd) *option { + add := func(depth int, row string, kind provider.Kind, path []string, account, fileIcon, key, mark string, run, reload func(*Model) tea.Cmd) *option { n := m.store.Node(key) sort := strings.ToLower(strings.Join(path, "/")) + " " + account es = append(es, entry{depth, sort, - option{label: m.pathLabel(kind, path, account), note: n.Note, star: key, + option{label: m.pathLabel(kind, path, account, fileIcon), note: n.Note, star: key, starAccount: cachefile.ProviderOf(row), starred: n.StarredFor(cachefile.ProviderOf(row)), mark: mark, run: run, reload: reload, id: sort, group: row, kind: kind}}) return &es[len(es)-1].opt @@ -81,7 +81,7 @@ func (m *Model) searchEntries() (sources, items []option) { } src.Provider, src.ProviderLabel = p.ID(), p.Label() row := sourceRowKey(p.ID(), src.ID) - dim(src.Hidden, add(0, row, src.Kind, []string{src.Name}, accountOf(src), sourceKey(src), m.sourceMark(row), func(m *Model) tea.Cmd { + dim(src.Hidden, add(0, row, src.Kind, []string{src.Name}, accountOf(src), "", sourceKey(src), m.sourceMark(row), func(m *Model) tea.Cmd { m.nav = &nav{sourceRow: row, sourceOnly: true} return nil }, func(m *Model) tea.Cmd { return m.loadSource(src) })) @@ -94,7 +94,7 @@ func (m *Model) searchEntries() (sources, items []option) { continue } reload := func(m *Model) tea.Cmd { return m.loadContainer(c) } - dim(c.Hidden, add(1, row, c.Source.Kind, []string{src.Name, c.Name}, accountOf(src), storeKey(c), m.vaultMark(c), func(m *Model) tea.Cmd { + dim(c.Hidden, add(1, row, c.Source.Kind, []string{src.Name, c.Name}, accountOf(src), m.fileIcon(m.st, c), storeKey(c), m.vaultMark(c), func(m *Model) tea.Cmd { m.startNav(r, actGo, row) return nil }, reload)) @@ -109,7 +109,7 @@ func (m *Model) searchEntries() (sources, items []option) { typ := strings.TrimSuffix(strings.ToLower(pluralLabels[t]), "s") // "key" suffix = strings.Trim(typ+" · "+suffix, " ·") } - e := add(2, row, c.Source.Kind, []string{src.Name, c.Name, o.Name}, suffix, objectKey(o), " ", func(m *Model) tea.Cmd { + e := add(2, row, c.Source.Kind, []string{src.Name, c.Name, o.Name}, suffix, m.fileIcon(m.st, c), objectKey(o), " ", func(m *Model) tea.Cmd { m.startNav(r, actGo, row) return nil }, reload)