Lay overview out as a table
The status field was not a column: "*", "↑2", "✓" and "✓ (no upstream)" are four different widths, so everything after them started somewhere else on every line and the eye had to hunt along each row instead of going down one. Each field now has its own measured column: name (with the branch appended when it is not master/main), a one-character dirty marker, the sync state, host and age, then the mirrors. "(no upstream)" was fifteen columns wide for something that is not even a problem, and is now "–". Colour weights the row rather than decorating it -- a project that is clean and in sync goes grey, the arrows and the dirty marker keep their colour -- and the rows needing action sort to the top, alphabetically within each group so positions stay predictable. padRight counted bytes, which was fine while everything it padded was ASCII; the arrows and check marks are three bytes and one column, so it counts runes now. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+64
-2
@@ -9,6 +9,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
func TestSanitizeComment(t *testing.T) {
|
||||
@@ -806,6 +807,7 @@ func TestTruthy(t *testing.T) {
|
||||
func TestFormatProjStatus(t *testing.T) {
|
||||
useColor = false
|
||||
defer func() { useColor = false }()
|
||||
w := overviewWidths{label: 12, sync: 5, host: 7}
|
||||
cases := []struct {
|
||||
s projStatus
|
||||
contains []string
|
||||
@@ -820,10 +822,15 @@ func TestFormatProjStatus(t *testing.T) {
|
||||
{projStatus{name: "d", branch: "feature", dirty: true},
|
||||
[]string{"d", "*", "(feature)"}, nil},
|
||||
{projStatus{name: "e", branch: "master"}, // clean, no upstream
|
||||
[]string{"e", "no upstream"}, []string{"*"}},
|
||||
[]string{"e", "–"}, []string{"*", "✓"}},
|
||||
{projStatus{name: "f", branch: "master", hasUpstream: true, ahead: 1, behind: 2},
|
||||
[]string{"f", "↑1↓2"}, []string{"✓"}}, // diverged shows both
|
||||
{projStatus{name: "g", branch: "master", hasUpstream: true, lastHost: "laptop",
|
||||
mirrors: []string{"hub", "gitea"}},
|
||||
[]string{"g", "laptop", "→ hub gitea"}, nil},
|
||||
}
|
||||
for _, c := range cases {
|
||||
got := formatProjStatus(c.s, 8)
|
||||
got := formatProjStatus(c.s, w)
|
||||
for _, sub := range c.contains {
|
||||
if !strings.Contains(got, sub) {
|
||||
t.Errorf("formatProjStatus(%+v) = %q, missing %q", c.s, got, sub)
|
||||
@@ -837,6 +844,61 @@ func TestFormatProjStatus(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestOverviewColumnsAlign is the point of the table: every field has to start
|
||||
// at the same column on every row, whatever the name lengths or the multi-byte
|
||||
// status glyphs do.
|
||||
func TestOverviewColumnsAlign(t *testing.T) {
|
||||
useColor = false
|
||||
// host names must not occur anywhere else in a row, or the index search
|
||||
// below would find them inside a project or branch name instead
|
||||
rows := []projStatus{
|
||||
{name: "a", branch: "master", hasUpstream: true, ahead: 12, behind: 3, lastHost: "workstation"},
|
||||
{name: "a-very-long-project-name", branch: "wip", dirty: true, lastHost: "buildbox"},
|
||||
{name: "mid", branch: "main", hasUpstream: true, lastHost: "laptop"},
|
||||
}
|
||||
w := measureOverview(rows)
|
||||
|
||||
var widths []int
|
||||
for _, r := range rows {
|
||||
line := formatProjStatus(r, w)
|
||||
// the host column starts right after the padded sync field
|
||||
idx := strings.Index(line, r.lastHost)
|
||||
if idx < 0 {
|
||||
t.Fatalf("host %q missing from %q", r.lastHost, line)
|
||||
}
|
||||
widths = append(widths, utf8.RuneCountInString(line[:idx]))
|
||||
}
|
||||
for i := 1; i < len(widths); i++ {
|
||||
if widths[i] != widths[0] {
|
||||
t.Errorf("host column starts at %d on row %d, %d on row 0:\n%s",
|
||||
widths[i], i, widths[0], strings.Join([]string{
|
||||
formatProjStatus(rows[0], w), formatProjStatus(rows[i], w)}, "\n"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAttentionRank(t *testing.T) {
|
||||
needs := []projStatus{
|
||||
{dirty: true},
|
||||
{ahead: 1},
|
||||
{behind: 1},
|
||||
}
|
||||
quiet := []projStatus{
|
||||
{hasUpstream: true},
|
||||
{}, // clean, no upstream: nothing to do about it here
|
||||
}
|
||||
for _, s := range needs {
|
||||
if attentionRank(s) != 0 {
|
||||
t.Errorf("%+v should sort to the top", s)
|
||||
}
|
||||
}
|
||||
for _, s := range quiet {
|
||||
if attentionRank(s) != 1 {
|
||||
t.Errorf("%+v should sort below the ones needing action", s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetectRemoteKind(t *testing.T) {
|
||||
cases := []struct {
|
||||
url, override string
|
||||
|
||||
Reference in New Issue
Block a user