From f16f61bacfeaa3055a534208f0e9532390abbcd9 Mon Sep 17 00:00:00 2001 From: "Mason J. Katz" Date: Fri, 21 Aug 2026 18:08:27 -0700 Subject: [PATCH 1/2] fix: preserve annotations across table groups Fixes #2 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- table_test.go | 40 ++++++++++++++++++++++++++++++++++++++++ text.go | 49 +++++++++++++++++++++++++++++++++++++------------ 2 files changed, 77 insertions(+), 12 deletions(-) diff --git a/table_test.go b/table_test.go index 0568bf0..4788ef5 100644 --- a/table_test.go +++ b/table_test.go @@ -535,6 +535,46 @@ func TestAnnotations(t *testing.T) { } } +func TestAnnotationsAtTypeBoundary(t *testing.T) { + type first struct { + Name string + } + + type second struct { + Value int + } + + var buf bytes.Buffer + + tbl := New(WithWriter(&buf)) + tbl.Write(first{Name: "first"}) + tbl.Annotate("between tables") + tbl.Write(second{Value: 2}) + _ = tbl.Flush() + + if output := buf.String(); !strings.Contains(output, "between tables") { + t.Errorf("expected boundary annotation, got: %q", output) + } +} + +func TestMultipleAnnotationsAtSamePosition(t *testing.T) { + var buf bytes.Buffer + + tbl := New(WithWriter(&buf)) + tbl.Annotate("first annotation") + tbl.Annotate("second annotation") + tbl.Write(server{Name: "test"}) + _ = tbl.Flush() + + output := buf.String() + first := strings.Index(output, "first annotation") + second := strings.Index(output, "second annotation") + + if first == -1 || second == -1 || first > second { + t.Errorf("expected ordered annotations, got: %q", output) + } +} + func TestClear(t *testing.T) { var buf bytes.Buffer diff --git a/text.go b/text.go index d6e65a1..34149d2 100644 --- a/text.go +++ b/text.go @@ -30,9 +30,11 @@ type wrapper interface { // applied. func (t *Table) FlushText() { var ( - prevType reflect.Type - columns []columnInfo - cells [][]cell + prevType reflect.Type + columns []columnInfo + cells [][]cell + groupStart int + includeGroupStart = true ) // This is the first pass through the table to determine the column widths @@ -46,8 +48,10 @@ func (t *Table) FlushText() { prevType = currType if columns != nil { // flush and reset for next table - t.flush(columns, cells) + t.flush(columns, cells, t.annotationsFor(groupStart, i, includeGroupStart)) cells = nil + groupStart = i + includeGroupStart = false } columns = t.processHeader(currType) @@ -88,21 +92,16 @@ func (t *Table) FlushText() { cells = append(cells, fields) } - t.flush(columns, cells) + t.flush(columns, cells, t.annotationsFor(groupStart, len(t.rows), includeGroupStart)) } -func (t *Table) flush(info []columnInfo, rows [][]cell) { +func (t *Table) flush(info []columnInfo, rows [][]cell, annotations []annotation) { t.flushHeader(info, rows) - annotations := t.annotations - // This pass applies ANSI styles and prints the table rows. for i := range rows { - if len(annotations) > 0 && annotations[0].index == i { - fmt.Fprintln(t.writer, sgr.Wrap(t.colors.Annotation, annotations[0].text)) - annotations = annotations[1:] // remove the annotation - } + annotations = t.flushAnnotations(annotations, i) var repeats []bool @@ -151,6 +150,17 @@ func (t *Table) flush(info []columnInfo, rows [][]cell) { fmt.Fprintln(t.writer) } + + t.flushAnnotations(annotations, len(rows)) +} + +func (t *Table) flushAnnotations(annotations []annotation, index int) []annotation { + for len(annotations) > 0 && annotations[0].index == index { + fmt.Fprintln(t.writer, sgr.Wrap(t.colors.Annotation, annotations[0].text)) + annotations = annotations[1:] + } + + return annotations } func (t *Table) flushHeader(info []columnInfo, rows [][]cell) { @@ -193,6 +203,21 @@ func (t *Table) flushHeader(info []columnInfo, rows [][]cell) { fmt.Fprintln(t.writer) } +func (t *Table) annotationsFor(start, end int, includeStart bool) []annotation { + var annotations []annotation + + for _, a := range t.annotations { + if a.index > end || (!includeStart && a.index == start) || a.index < start { + continue + } + + a.index -= start + annotations = append(annotations, a) + } + + return annotations +} + func (t *Table) processHeader(header reflect.Type) []columnInfo { numFields := header.NumField() From 2a8e909344ee269e7e330d8c66d5622ea3a909ad Mon Sep 17 00:00:00 2001 From: "Mason J. Katz" Date: Fri, 21 Aug 2026 18:24:49 -0700 Subject: [PATCH 2/2] ci: pin shared workflows to v1.3.1 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/test.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 99db0d5..3e8d465 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -10,7 +10,7 @@ on: jobs: lint: - uses: endobit/ci/.github/workflows/go-lint.yaml@main + uses: endobit/ci/.github/workflows/go-lint.yaml@v1.3.1 test: - uses: endobit/ci/.github/workflows/go-test.yaml@main + uses: endobit/ci/.github/workflows/go-test.yaml@v1.3.1 secrets: inherit