Commit 3d834e058d
3d834e058d3bdff6dc7e34020b304304085b001e
parent: c1ccabf7ee
Verified · cmc
cmc <hello@cleberg.net> · 2026-09-24 01:13 UTC
table: keep cells beyond the header
Ref #254
internal/control/table.go
+16 −7
| @@ -72,13 +72,19 @@ func (t *table) flush() { |
| 72 | 72 | if t.term.Cols == 0 || len(t.rows) == 0 { |
| 73 | 73 | return |
| 74 | 74 | } |
| 75 | // The column count is never smaller than the longest row: a row with |
| 76 | // more cells than the header has still gets every cell rendered, the |
| 77 | // header just shows blank above the ones it doesn't name. |
| 75 | 78 | n := len(t.header) |
| 79 | for _, r := range t.rows { |
| 80 | n = max(n, len(r)) |
| 81 | } |
| 76 | 82 | widths := make([]int, n) |
| 77 | 83 | for i, h := range t.header { |
| 78 | 84 | widths[i] = cells(h) |
| 79 | 85 | } |
| 80 | 86 | for _, r := range t.rows { |
| 81 | | for i := 0; i < n && i < len(r); i++ { |
| 87 | for i := 0; i < len(r); i++ { |
| 82 | 88 | widths[i] = max(widths[i], cells(r[i].s)) |
| 83 | 89 | } |
| 84 | 90 | } |
| @@ -86,8 +92,10 @@ func (t *table) flush() { |
| 86 | 92 | |
| 87 | 93 | var b strings.Builder |
| 88 | 94 | line := make([]string, n) |
| 89 | | for i, h := range t.header { |
| 90 | | line[i] = h |
| 95 | for i := range line { |
| 96 | if i < len(t.header) { |
| 97 | line[i] = t.header[i] |
| 98 | } |
| 91 | 99 | } |
| 92 | 100 | b.WriteString(t.term.paint(sgrDim, t.join(line, widths)) + "\n") |
| 93 | 101 | for _, r := range t.rows { |
| @@ -115,10 +123,11 @@ func (t *table) fit(widths []int) { |
| 115 | 123 | return s |
| 116 | 124 | } |
| 117 | 125 | kinds := make([]cellKind, len(widths)) |
| 118 | | if len(t.rows) > 0 { |
| 119 | | for i := range widths { |
| 120 | | if i < len(t.rows[0]) { |
| 121 | | kinds[i] = t.rows[0][i].kind |
| 126 | for i := range widths { |
| 127 | for _, r := range t.rows { |
| 128 | if i < len(r) { |
| 129 | kinds[i] = r[i].kind |
| 130 | break |
| 122 | 131 | } |
| 123 | 132 | } |
| 124 | 133 | } |
internal/control/table_test.go
+31
| @@ -70,6 +70,37 @@ func TestTableAgesAndPlainStamps(t *testing.T) { |
| 70 | 70 | } |
| 71 | 71 | } |
| 72 | 72 | |
| 73 | // A row may carry a cell beyond what the header names — repo list's |
| 74 | // trailing [archived] marker, only present on some rows. flush must |
| 75 | // still render it, not silently drop it because it falls past |
| 76 | // len(header). |
| 77 | func TestTableKeepsCellsBeyondTheHeader(t *testing.T) { |
| 78 | var b bytes.Buffer |
| 79 | tb := (&Ctx{Term: Term{Cols: 80}}).table(&b, "PATH", "VISIBILITY", "DESCRIPTION") |
| 80 | tb.row(cRef("a/x"), cState("public"), cFlex("one")) |
| 81 | tb.row(cRef("a/y"), cState("public"), cFlex("two"), cText("[archived]")) |
| 82 | tb.flush() |
| 83 | |
| 84 | out := b.String() |
| 85 | if !strings.Contains(out, "[archived]") { |
| 86 | t.Fatalf("archived marker dropped:\n%s", out) |
| 87 | } |
| 88 | lines := strings.Split(strings.TrimRight(out, "\n"), "\n") |
| 89 | if len(lines) != 3 { |
| 90 | t.Fatalf("want 3 lines, got %d:\n%s", len(lines), out) |
| 91 | } |
| 92 | header, row1, row2 := lines[0], lines[1], lines[2] |
| 93 | descAt := strings.Index(header, "DESCRIPTION") |
| 94 | oneAt := strings.Index(row1, "one") |
| 95 | twoAt := strings.Index(row2, "two") |
| 96 | if descAt < 0 || oneAt < 0 || twoAt < 0 { |
| 97 | t.Fatalf("columns not found:\n%s", out) |
| 98 | } |
| 99 | if descAt != oneAt || descAt != twoAt { |
| 100 | t.Errorf("DESCRIPTION column not aligned: header at %d, row1 at %d, row2 at %d\n%s", descAt, oneAt, twoAt, out) |
| 101 | } |
| 102 | } |
| 103 | |
| 73 | 104 | func TestTableEmptyPrintsNothing(t *testing.T) { |
| 74 | 105 | var b bytes.Buffer |
| 75 | 106 | (&Ctx{Term: Term{Cols: 80}}).table(&b, "#").flush() |