diff --git a/docs/keys.md b/docs/keys.md index d087f15..3d4185a 100644 --- a/docs/keys.md +++ b/docs/keys.md @@ -113,7 +113,8 @@ Secrets. Going into it (`l`, or `4`) keeps it on that; there `j`/`k`, it's hidden. On top, the status pane (`0`) shows the version, a newer release and how -your accounts are doing. Focused, the right pane lists more: the version +your accounts are doing. Focused, the right pane lists more under the +logo (when there's room): the version and updates (`U`), each account (`L` on one that needs it signs in), the config, state and cache files (`e` opens one in your editor), and links to the docs, source, issues, releases and changelog (`o` opens one; on diff --git a/internal/ui/app/app_test.go b/internal/ui/app/app_test.go index deac928..56bd1b2 100644 --- a/internal/ui/app/app_test.go +++ b/internal/ui/app/app_test.go @@ -1190,7 +1190,7 @@ func TestStatusPane(t *testing.T) { t.Fatalf("focus %v", d.m.focus) } scr := d.screen() - for _, want := range []string{"version v0.1.0", "GPL-3.0-or-later", "Azure · 2 subscriptions", "Azure · admin · sign-in required", + for _, want := range []string{logo[2], "version v0.1.0", "GPL-3.0-or-later", "Azure · 2 subscriptions", "Azure · admin · sign-in required", "/cfg/config.yaml", "/state/state.json", "lazysecrets/…/docs"} { if !strings.Contains(scr, want) { t.Errorf("missing %q:\n%s", want, scr) @@ -1216,6 +1216,11 @@ func TestStatusPane(t *testing.T) { if len(opened) != 1 || !strings.HasSuffix(opened[0], "/src/branch/main/docs") { t.Errorf("o opened %q", opened) } + d.m.fields.SelectWhere(func(f panes.Field) bool { return f.Label == "version" }) + d.keys("o") + if len(opened) != 2 || !strings.HasSuffix(opened[1], "/releases/tag/v0.1.0") { + t.Errorf("o on the version opened %q", opened) + } d.keys("shift+tab") if d.m.focus != paneStatus { t.Errorf("back to %v", d.m.focus) diff --git a/internal/ui/app/fields.go b/internal/ui/app/fields.go index bb6fa6f..d06f0b6 100644 --- a/internal/ui/app/fields.go +++ b/internal/ui/app/fields.go @@ -50,6 +50,7 @@ func (m *Model) syncFields() tea.Cmd { m.fields.Hide() return nil } + m.fields.Header, m.fields.HeaderShort = nil, "" switch m.detailOf() { case paneSources: r, ok := m.sources.Selected() @@ -68,6 +69,7 @@ func (m *Model) syncFields() tea.Cmd { m.fields.Show("", nil) } case paneStatus: + m.fields.Header, m.fields.HeaderShort = logo, strings.TrimSpace("lazysecrets "+m.versionLabel()) m.fields.Show("lazysecrets", m.statusFields()) case paneContainers: if c, ok := m.containers.Selected(); ok { diff --git a/internal/ui/app/status.go b/internal/ui/app/status.go index c3da3e5..4e2de7f 100644 --- a/internal/ui/app/status.go +++ b/internal/ui/app/status.go @@ -16,6 +16,16 @@ import ( // (0), the right pane has the rest: updates, each account, the files // lazysecrets uses, links, and the changelog. +// logo heads the status pane's rows. +var logo = []string{ + ` _ _`, + `| | __ _ _____ _ ___ ___ ___ _ __ ___| |_ ___`, + `| |/ _` + "`" + ` |_ / | | / __|/ _ \/ __| '__/ _ \ __/ __|`, + `| | (_| |/ /| |_| \__ \ __/ (__| | | __/ |_\__ \`, + `|_|\__,_/___|\__, |___/\___|\___|_| \___|\__|___/`, + ` |___/`, +} + // statusH is the status pane's height: one line in a box. const statusH = 3 diff --git a/internal/ui/panes/list.go b/internal/ui/panes/list.go index 0b9478f..c1feb06 100644 --- a/internal/ui/panes/list.go +++ b/internal/ui/panes/list.go @@ -37,6 +37,11 @@ type List[T any] struct { // Age says how old the items are ("2h ago") while they reload, by // the counter. Age string + // Header lines (a logo) go above the items while the cursor is in + // the rows under it, the pane has room and there's no filter; else + // HeaderShort, a line of plain text, does. + Header []string + HeaderShort string item Item[T] items []T @@ -163,9 +168,36 @@ func (l List[T]) rows() int { if l.filtering { r-- } + r -= len(l.header()) return max(0, r) } +// headerMinRows is how many item rows Header leaves at least. +const headerMinRows = 5 + +// header is the lines above the items: Header and a blank line, if it +// fits, leaves headerMinRows and the cursor is in the rows under it; +// else HeaderShort. +func (l List[T]) header() []string { + short := []string{} + if l.HeaderShort != "" { + short = []string{l.HeaderShort} + } + if len(l.Header) == 0 || l.filtering || l.input.Value() != "" { + return short + } + for _, line := range l.Header { + if ansi.StringWidth(line) > l.w-3 { + return short + } + } + below := l.h - 2 - len(l.Header) - 1 + if below < headerMinRows || l.cursor >= below { + return short + } + return append(l.Header[:len(l.Header):len(l.Header)], "") +} + // Filtering reports whether the filter input has focus. func (l List[T]) Filtering() bool { return l.filtering } @@ -329,6 +361,9 @@ func (l List[T]) View(st styles.Styles, focused bool, spin string) string { if l.filtering { body = append(body, l.input.View()) } + for _, line := range l.header() { + body = append(body, st.Notice.Render(" "+line)) + } switch { case l.Loading && len(l.items) == 0: diff --git a/internal/ui/panes/list_test.go b/internal/ui/panes/list_test.go index 2b2209d..aa8aafc 100644 --- a/internal/ui/panes/list_test.go +++ b/internal/ui/panes/list_test.go @@ -177,3 +177,27 @@ func TestHighlightKeepsColours(t *testing.T) { t.Error("reverse kept the colours") } } + +func TestListHeader(t *testing.T) { + l := NewList("t", Item[string]{ + ID: func(s string) string { return s }, + Filter: func(s string) string { return s }, + Render: func(_ styles.Styles, s string, _ int) string { return s }, + }) + l.Header, l.HeaderShort = []string{"ART1", "ART2"}, "short" + l.SetItems([]string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j"}) + l.SetSize(30, 2+2+1+6) // the art, a blank line and 6 rows + st := styles.Default() + if v := l.View(st, true, ""); !strings.Contains(v, "ART1") || strings.Contains(v, "short") { + t.Errorf("at the top, the art:\n%s", v) + } + l.Move(6) // below the rows under the art + if v := l.View(st, true, ""); strings.Contains(v, "ART1") || !strings.Contains(v, "short") || !strings.Contains(v, " g") { + t.Errorf("further down, the text:\n%s", v) + } + l.Top() + l.SetSize(30, 8) // too short for the art and 5 rows + if v := l.View(st, true, ""); strings.Contains(v, "ART1") || !strings.Contains(v, "short") { + t.Errorf("no room, the text:\n%s", v) + } +}