Skip to content

Commit 7c452a7

Browse files
wpjuniorclaude
andcommitted
Replace tabwriter with custom implementation for ANSI color support
The standard library tabwriter doesn't account for ANSI escape codes when calculating column widths, causing misaligned output with colored text. This replaces it with a custom implementation that uses runeLen() to properly calculate display widths. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 211e02a commit 7c452a7

2 files changed

Lines changed: 97 additions & 36 deletions

File tree

render.go

Lines changed: 61 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,11 @@ package tablecli
66

77
import (
88
"bytes"
9-
"fmt"
10-
"io"
119
"os"
1210
"reflect"
1311
"regexp"
1412
"sort"
1513
"strings"
16-
"text/tabwriter"
1714
"unicode"
1815
"unicode/utf8"
1916

@@ -221,60 +218,88 @@ func (t *Table) resizeLargestColumn(ttyWidth int) []int {
221218
return t.columnsSize()
222219
}
223220

224-
const (
225-
tabwriterMinWidth = 6
226-
tabwriterWidth = 4
227-
tabwriterPadding = 3
228-
tabwriterPadChar = ' '
229-
)
230-
231-
// getNewTabWriter returns a tabwriter that translates tabbed columns in input into properly aligned text.
232-
func getNewTabWriter(output io.Writer) *tabwriter.Writer {
233-
return tabwriter.NewWriter(output, tabwriterMinWidth, tabwriterWidth, tabwriterPadding, tabwriterPadChar, 0)
234-
}
235-
236221
var tableWriterReplacer = strings.NewReplacer(
237222
"\f", " ",
238223
"\n", " ",
239224
"\r", " ",
240225
)
241226

242-
func (t *Table) renderUsingTabWriter() string {
243-
buf := bytes.NewBuffer(nil)
244-
w := getNewTabWriter(buf)
227+
func (t *Table) renderUsingTabWriterLike() string {
245228
padding := strings.Repeat(" ", t.TableWriterPadding)
246229

247-
if len(t.Headers) > 0 {
248-
capitalizedHeaders := []string{}
249-
for _, header := range t.Headers {
250-
capitalizedHeaders = append(capitalizedHeaders, strings.ToUpper(header))
230+
// Process rows and calculate column widths
231+
processedRows := make([][]string, len(t.rows))
232+
for i, row := range t.rows {
233+
processedRows[i] = make([]string, len(row))
234+
for j, col := range row {
235+
if idx := strings.IndexAny(col, "\f\n\r"); idx >= 0 {
236+
if TableConfig.TabWriterTruncate || t.TableWriterTruncate {
237+
col = col[:idx] + " ..."
238+
} else {
239+
col = tableWriterReplacer.Replace(col)
240+
}
241+
}
242+
processedRows[i][j] = col
251243
}
252-
fmt.Fprintln(w, padding+strings.Join(capitalizedHeaders, "\t"))
253244
}
254245

255-
for _, row := range t.rows {
256-
newRow := make([]string, len(row))
257-
for i, column := range row {
258-
breakchar := strings.IndexAny(column, "\f\n\r")
259-
if breakchar >= 0 {
260-
if TableConfig.TabWriterTruncate || t.TableWriterTruncate {
261-
column = column[:breakchar] + " ..."
262-
} else {
263-
column = tableWriterReplacer.Replace(column)
246+
// Calculate column widths
247+
var numCols int
248+
if len(t.Headers) > 0 {
249+
numCols = len(t.Headers)
250+
} else if len(processedRows) > 0 {
251+
numCols = len(processedRows[0])
252+
}
253+
widths := make([]int, numCols)
254+
for i, h := range t.Headers {
255+
if w := runeLen(h); w > widths[i] {
256+
widths[i] = w
257+
}
258+
}
259+
for _, row := range processedRows {
260+
for i, col := range row {
261+
if i < numCols {
262+
if w := runeLen(col); w > widths[i] {
263+
widths[i] = w
264264
}
265265
}
266+
}
267+
}
266268

267-
newRow[i] = column
269+
// Build output
270+
var buf strings.Builder
271+
if len(t.Headers) > 0 {
272+
buf.WriteString(padding)
273+
for i, h := range t.Headers {
274+
if i > 0 {
275+
buf.WriteString(" ")
276+
}
277+
buf.WriteString(strings.ToUpper(h))
278+
if i < numCols-1 {
279+
buf.WriteString(strings.Repeat(" ", widths[i]-runeLen(h)))
280+
}
281+
}
282+
buf.WriteString("\n")
283+
}
284+
for _, row := range processedRows {
285+
buf.WriteString(padding)
286+
for i, col := range row {
287+
if i > 0 {
288+
buf.WriteString(" ")
289+
}
290+
buf.WriteString(col)
291+
if i < numCols-1 {
292+
buf.WriteString(strings.Repeat(" ", widths[i]-runeLen(col)))
293+
}
268294
}
269-
fmt.Fprintln(w, padding+strings.Join(newRow, "\t"))
295+
buf.WriteString("\n")
270296
}
271-
w.Flush()
272297
return buf.String()
273298
}
274299

275300
func (t *Table) String() string {
276301
if TableConfig.UseTabWriter {
277-
return t.renderUsingTabWriter()
302+
return t.renderUsingTabWriterLike()
278303
}
279304
if t.Headers == nil && len(t.rows) < 1 {
280305
return ""

render_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -599,6 +599,42 @@ Mixed a b c d
599599
assert.Equal(t, expected, table.String())
600600
}
601601

602+
func TestStringTabWriterWithANSIColors(t *testing.T) {
603+
TableConfig.UseTabWriter = true
604+
defer func() {
605+
TableConfig.UseTabWriter = false
606+
}()
607+
table := NewTable()
608+
table.Headers = Row{"ID", "Status", "Name"}
609+
// ANSI color codes: \033[33;1;1m = yellow, \033[0m = reset
610+
yellow := func(s string) string { return "\033[33;1;1m" + s + "\033[0m" }
611+
red := func(s string) string { return "\033[31;1;1m" + s + "\033[0m" }
612+
table.AddRow(Row{"abc123", "ok", "app1"})
613+
table.AddRow(Row{yellow("def456"), yellow("running"), yellow("…")})
614+
table.AddRow(Row{red("ghi789"), red("error"), red("app3")})
615+
output := table.String()
616+
617+
// Verify alignment is correct (columns should align despite ANSI codes)
618+
lines := strings.Split(output, "\n")
619+
assert.Len(t, lines, 5) // header + 3 rows + trailing newline
620+
621+
// Check that ANSI codes are preserved in output
622+
assert.Contains(t, output, "\033[33;1;1m")
623+
assert.Contains(t, output, "\033[31;1;1m")
624+
assert.Contains(t, output, "\033[0m")
625+
626+
// Verify the colored content is present
627+
assert.Contains(t, output, "def456")
628+
assert.Contains(t, output, "running")
629+
assert.Contains(t, output, "error")
630+
assert.Contains(t, output, "…")
631+
632+
assert.Equal(t, 23, runeLen(lines[1])) // First row
633+
assert.Equal(t, 20, runeLen(lines[2])) // Second row
634+
assert.Equal(t, 23, runeLen(lines[3])) // Third row
635+
assert.Equal(t, 0, runeLen(lines[4])) // Trailing newline
636+
}
637+
602638
func BenchmarkString(b *testing.B) {
603639
b.StopTimer()
604640
table := NewTable()

0 commit comments

Comments
 (0)