Archived
Make the screen explain itself: labels, tools, help pages, hints
For developers new to submodules (notes/usability-plan.md, D32): - x and X read "restore to pin" and "restore committed pins" - problems only, filter and fetch are shown in the footer - ? shows every key in titled columns, then a legend of the status words, symbols and spine roles; esc closes it - u reads "pull (fast-forward)" on the superproject - the detail pane suggests f when the last fetch is old - a hint above the footer until the first key - the header says "staged", like the tree Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
||||
"path"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/bubbles/help"
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
@@ -18,6 +19,9 @@ import (
|
||||
|
||||
const labelWidth = 11
|
||||
|
||||
// staleFetch is the age of the last fetch from which the pane suggests f.
|
||||
const staleFetch = 24 * time.Hour
|
||||
|
||||
type Model struct {
|
||||
theme theme.Theme
|
||||
width int
|
||||
@@ -38,6 +42,9 @@ func New(t theme.Theme, actions []key.Binding) Model {
|
||||
// PaletteVisible reports whether the palette of submodule actions fits.
|
||||
func (m Model) PaletteVisible() bool { return m.height >= paletteMinHeight }
|
||||
|
||||
// SetActions replaces the bindings of the palette.
|
||||
func (m *Model) SetActions(actions []key.Binding) { m.actions = actions }
|
||||
|
||||
func (m *Model) SetSize(w, h int) { m.width, m.height = w, h }
|
||||
func (m *Model) SetSpinner(frame string) { m.spinner = frame }
|
||||
|
||||
@@ -123,10 +130,17 @@ func (m Model) lines(s *submodule.Submodule, repo *submodule.Repo) []string {
|
||||
st := s.Status
|
||||
|
||||
// the age of the last fetch is right-aligned in the name's line
|
||||
// with a hint to fetch if the last fetch is old, and room for it
|
||||
if st.Presence == submodule.PresenceOK {
|
||||
fetched := "fetched " + theme.Ago(st.FetchedAt)
|
||||
if gap := m.width - ansi.StringWidth(s.Name) - len(fetched); gap >= 2 {
|
||||
out[1] += strings.Repeat(" ", gap) + muted.Render(fetched)
|
||||
fetched := muted.Render("fetched " + theme.Ago(st.FetchedAt))
|
||||
if time.Since(st.FetchedAt) > staleFetch {
|
||||
hinted := fetched + muted.Render(" · ") + text.Bold(true).Render("f") + muted.Render(" fetch")
|
||||
if m.width-ansi.StringWidth(s.Name)-lipgloss.Width(hinted) >= 2 {
|
||||
fetched = hinted
|
||||
}
|
||||
}
|
||||
if gap := m.width - ansi.StringWidth(s.Name) - lipgloss.Width(fetched); gap >= 2 {
|
||||
out[1] += strings.Repeat(" ", gap) + fetched
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package detail
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/x/ansi"
|
||||
|
||||
"submarine/internal/submodule"
|
||||
"submarine/internal/tui/theme"
|
||||
)
|
||||
|
||||
func TestFetchHintForOldFetches(t *testing.T) {
|
||||
m := New(theme.Default(), nil)
|
||||
m.SetSize(60, 20)
|
||||
for _, c := range []struct {
|
||||
fetched time.Time
|
||||
hint bool
|
||||
}{
|
||||
{time.Time{}, true},
|
||||
{time.Now().Add(-48 * time.Hour), true},
|
||||
{time.Now().Add(-time.Hour), false},
|
||||
} {
|
||||
s := &submodule.Submodule{Path: "src/app", Name: "app", Status: &submodule.Status{
|
||||
Presence: submodule.PresenceOK, FetchedAt: c.fetched, Pin: submodule.PinInfo{Pushed: true},
|
||||
}}
|
||||
got := ansi.Strip(m.View(s, nil))
|
||||
if strings.Contains(got, "f fetch") != c.hint {
|
||||
t.Errorf("fetched %s: hint %v, want %v:\n%s", theme.Ago(c.fetched), !c.hint, c.hint, got)
|
||||
}
|
||||
if !strings.Contains(got, "fetched "+theme.Ago(c.fetched)) {
|
||||
t.Errorf("the age is missing:\n%s", got)
|
||||
}
|
||||
}
|
||||
|
||||
// too narrow for the hint: the age alone
|
||||
m.SetSize(24, 20)
|
||||
s := &submodule.Submodule{Path: "src/app", Name: "app", Status: &submodule.Status{Presence: submodule.PresenceOK}}
|
||||
if got := ansi.Strip(m.View(s, nil)); strings.Contains(got, "f fetch") || !strings.Contains(got, "fetched never") {
|
||||
t.Errorf("narrow pane:\n%s", got)
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ func TestPaletteWrapsInsteadOfCuttingKeys(t *testing.T) {
|
||||
key.NewBinding(key.WithKeys("c"), key.WithHelp("c", "copy path")),
|
||||
key.NewBinding(key.WithKeys("u"), key.WithHelp("u", "update pin")),
|
||||
key.NewBinding(key.WithKeys("b"), key.WithHelp("b", "switch branch")),
|
||||
key.NewBinding(key.WithKeys("x"), key.WithHelp("x", "restore")),
|
||||
key.NewBinding(key.WithKeys("x"), key.WithHelp("x", "restore to pin")),
|
||||
}
|
||||
m := New(theme.Default(), keys)
|
||||
m.SetSize(36, 20)
|
||||
@@ -24,7 +24,7 @@ func TestPaletteWrapsInsteadOfCuttingKeys(t *testing.T) {
|
||||
t.Fatalf("want 2 lines, got %q", lines)
|
||||
}
|
||||
all := strings.Join(lines, "\n")
|
||||
for _, want := range []string{"copy path", "update pin", "switch branch", "x restore"} {
|
||||
for _, want := range []string{"copy path", "update pin", "switch branch", "x restore to pin"} {
|
||||
if !strings.Contains(all, want) {
|
||||
t.Errorf("palette lacks %q: %q", want, lines)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user