diff --git a/internal/picker/styles.go b/internal/picker/styles.go index c9a7e7c..bdefa85 100644 --- a/internal/picker/styles.go +++ b/internal/picker/styles.go @@ -92,8 +92,11 @@ func (t theme) statusClusterOn(base lipgloss.Style, st git.Status, loaded bool) if !loaded { return f(t.muted, glyphLoading) } - // A Project's .git can still fail to read, or vanish after the Picker - // opened; git's answer is then as unknown as a timeout. + // A directory with no .git is not a failure: its row shows no marker. + if st.Kind == git.NotRepo { + return "" + } + // A repository whose git call failed or timed out is unknown. if st.Kind != git.Found { return f(t.red, glyphUnknown) } @@ -123,6 +126,9 @@ func (t theme) statusClusterOn(base lipgloss.Style, st git.Status, loaded bool) // statusClusterWidth is the plain (uncoloured) width of statusCluster's // output, for column alignment. func statusClusterWidth(st git.Status, loaded bool) int { + if loaded && st.Kind == git.NotRepo { + return 0 + } if !loaded || st.Kind != git.Found { return 1 } @@ -176,9 +182,9 @@ func previewStatusWords(t theme, st git.Status, loaded bool) string { } switch st.Kind { case git.NotRepo: - return t.fg(t.red).Render(glyphUnknown + " git could not read it") + return t.muted_().Render("not a git repo") case git.Unknown: - return t.fg(t.red).Render(glyphUnknown + " git timed out") + return t.fg(t.red).Render(glyphUnknown + " git could not read it") } var parts []string diff --git a/internal/picker/update_test.go b/internal/picker/update_test.go index 9a7a768..6347baf 100644 --- a/internal/picker/update_test.go +++ b/internal/picker/update_test.go @@ -67,7 +67,7 @@ func keyedStatus(statuses map[string]git.Status) picker.StatusFunc { func TestModel_Update_StatusResult(t *testing.T) { statuses := map[string]git.Status{ "/root/work/alpha": {Kind: git.Found, State: git.Dirty}, - "/root/work/beta": {Kind: git.NotRepo}, + "/root/work/beta": {Kind: git.Unknown}, } m := picker.NewModel( []picker.Row{ @@ -101,7 +101,7 @@ func TestModel_Update_StatusResult(t *testing.T) { // Feed the results through Update in reverse order: if results were // keyed by arrival index rather than by Project path, the last row's - // status (beta, NotRepo) would land on the first row instead. + // status (beta, Unknown) would land on the first row instead. for i := len(results) - 1; i >= 0; i-- { next, _ = final.Update(results[i]) final = next.(picker.Model) diff --git a/internal/picker/view_status_test.go b/internal/picker/view_status_test.go new file mode 100644 index 0000000..2fb5d0d --- /dev/null +++ b/internal/picker/view_status_test.go @@ -0,0 +1,53 @@ +package picker_test + +import ( + "strings" + "testing" + + tea "charm.land/bubbletea/v2" + + "github.com/kryft-dev/cdd/internal/git" + "github.com/kryft-dev/cdd/internal/picker" +) + +// statusView renders a one-row Picker whose status has landed as st, wide +// enough for the preview and short enough to drop the legend, so any "!" in +// the output comes from the row or the preview. +func statusView(t *testing.T, st git.Status) string { + t.Helper() + rows := []picker.Row{{Project: picker.Project{Dir: "~/work/", Name: "alpha", Path: "/root/work/alpha"}}} + m := picker.NewModel(rows, keyedStatus(map[string]git.Status{"/root/work/alpha": st}), picker.Options{}) + next, _ := m.Update(tea.WindowSizeMsg{Width: 120, Height: 10}) + m = next.(picker.Model) + for _, c := range m.Init()().(tea.BatchMsg) { + next, _ = m.Update(c()) + m = next.(picker.Model) + } + return plain(m.View().Content) +} + +// TestView_NotRepoIsNeutral verifies that a Project with no .git shows no +// "!" and a plain "not a git repo" in the preview, with no branch or sync. +func TestView_NotRepoIsNeutral(t *testing.T) { + out := statusView(t, git.Status{Kind: git.NotRepo}) + if strings.Contains(out, "!") { + t.Errorf("view has a !, want none for a non-git Project:\n%s", out) + } + if !strings.Contains(out, "not a git repo") { + t.Errorf("view lacks %q:\n%s", "not a git repo", out) + } + for _, word := range []string{"branch", "sync"} { + if strings.Contains(out, word) { + t.Errorf("view has %q, want it omitted for a non-git Project:\n%s", word, out) + } + } +} + +// TestView_UnknownStaysRed verifies that a real git failure keeps the "!" +// in the row and its words in the preview. +func TestView_UnknownStaysRed(t *testing.T) { + out := statusView(t, git.Status{Kind: git.Unknown}) + if !strings.Contains(out, "!") || !strings.Contains(out, "git could not read it") { + t.Errorf("view lacks the ! and %q for an Unknown status:\n%s", "git could not read it", out) + } +}