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() {
7272 if t.term.Cols == 0 || len(t.rows) == 0 {
7373 return
7474 }
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.
7578 n := len(t.header)
79 for _, r := range t.rows {
80 n = max(n, len(r))
81 }
7682 widths := make([]int, n)
7783 for i, h := range t.header {
7884 widths[i] = cells(h)
7985 }
8086 for _, r := range t.rows {
81 for i := 0; i < n && i < len(r); i++ {
87 for i := 0; i < len(r); i++ {
8288 widths[i] = max(widths[i], cells(r[i].s))
8389 }
8490 }
@@ -86,8 +92,10 @@ func (t *table) flush() {
8692
8793 var b strings.Builder
8894 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 }
9199 }
92100 b.WriteString(t.term.paint(sgrDim, t.join(line, widths)) + "\n")
93101 for _, r := range t.rows {
@@ -115,10 +123,11 @@ func (t *table) fit(widths []int) {
115123 return s
116124 }
117125 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
122131 }
123132 }
124133 }
internal/control/table_test.go +31
@@ -70,6 +70,37 @@ func TestTableAgesAndPlainStamps(t *testing.T) {
7070 }
7171}
7272
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).
77func 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
73104func TestTableEmptyPrintsNothing(t *testing.T) {
74105 var b bytes.Buffer
75106 (&Ctx{Term: Term{Cols: 80}}).table(&b, "#").flush()