Skip to content

Commit 53971e3

Browse files
wpjuniorclaude
andcommitted
Add TabWriter truncation and padding options
Inspired by kubectl's table output formatting, this adds TabWriterTruncate config to control multiline handling in tab writer mode - when enabled, truncates at first line break with "..."; when disabled, replaces break characters with spaces. Also adds per-table DisableTableWriterTruncate override and TableWriterPadding for leading whitespace. Includes handling for form feed and carriage return characters. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 4f0e67e commit 53971e3

2 files changed

Lines changed: 142 additions & 6 deletions

File tree

render.go

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,15 @@ var TableConfig = struct {
2525
ForceWrap bool
2626
UseTabWriter bool
2727
MaxTTYWidth int
28+
29+
TabWriterTruncate bool
2830
}{
2931
BreakOnAny: false,
3032
ForceWrap: false,
3133
UseTabWriter: false,
3234
MaxTTYWidth: 0,
35+
36+
TabWriterTruncate: true,
3337
}
3438

3539
var ignoredPatterns = []*regexp.Regexp{
@@ -43,6 +47,9 @@ type Table struct {
4347
Headers Row
4448
LineSeparator bool
4549
rows rowSlice
50+
51+
DisableTableWriterTruncate bool
52+
TableWriterPadding int
4653
}
4754

4855
type Row []string
@@ -226,24 +233,40 @@ func getNewTabWriter(output io.Writer) *tabwriter.Writer {
226233
return tabwriter.NewWriter(output, tabwriterMinWidth, tabwriterWidth, tabwriterPadding, tabwriterPadChar, 0)
227234
}
228235

236+
var tableWriterReplacer = strings.NewReplacer(
237+
"\f", " ",
238+
"\n", " ",
239+
"\r", " ",
240+
)
241+
229242
func (t *Table) renderUsingTabWriter() string {
230243
buf := bytes.NewBuffer(nil)
231244
w := getNewTabWriter(buf)
245+
padding := strings.Repeat(" ", t.TableWriterPadding)
232246

233247
if len(t.Headers) > 0 {
234248
capitalizedHeaders := []string{}
235249
for _, header := range t.Headers {
236250
capitalizedHeaders = append(capitalizedHeaders, strings.ToUpper(header))
237251
}
238-
fmt.Fprintln(w, strings.Join(capitalizedHeaders, "\t"))
252+
fmt.Fprintln(w, padding+strings.Join(capitalizedHeaders, "\t"))
239253
}
240254

241255
for _, row := range t.rows {
242256
newRow := make([]string, len(row))
243257
for i, column := range row {
244-
newRow[i] = strings.ReplaceAll(column, "\n", " ")
258+
breakchar := strings.IndexAny(column, "\f\n\r")
259+
if breakchar >= 0 {
260+
if TableConfig.TabWriterTruncate && !t.DisableTableWriterTruncate {
261+
column = column[:breakchar] + " ..."
262+
} else {
263+
column = tableWriterReplacer.Replace(column)
264+
}
265+
}
266+
267+
newRow[i] = column
245268
}
246-
fmt.Fprintln(w, strings.Join(newRow, "\t"))
269+
fmt.Fprintln(w, padding+strings.Join(newRow, "\t"))
247270
}
248271
w.Flush()
249272
return buf.String()

render_test.go

Lines changed: 116 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -476,9 +476,122 @@ func TestStringTabWriterMultiline(t *testing.T) {
476476
table.AddRow(Row{"One", "1", ""})
477477
table.AddRow(Row{"Two", "xxx\nyyy", "aa|bb|cc|dd"})
478478
table.AddRow(Row{"Three", "3", ""})
479-
expected := `One 1
480-
Two xxx yyy aa|bb|cc|dd
481-
Three 3
479+
expected := "One 1 \nTwo xxx ... aa|bb|cc|dd\nThree 3 \n"
480+
assert.Equal(t, expected, table.String())
481+
}
482+
483+
func TestStringTabWriterTruncateEnabled(t *testing.T) {
484+
TableConfig.UseTabWriter = true
485+
TableConfig.TabWriterTruncate = true
486+
defer func() {
487+
TableConfig.UseTabWriter = false
488+
TableConfig.TabWriterTruncate = true
489+
}()
490+
table := NewTable()
491+
table.Headers = Row{"Name", "Description"}
492+
table.AddRow(Row{"Item1", "Single line"})
493+
table.AddRow(Row{"Item2", "First line\nSecond line\nThird line"})
494+
table.AddRow(Row{"Item3", "Another single"})
495+
expected := `NAME DESCRIPTION
496+
Item1 Single line
497+
Item2 First line ...
498+
Item3 Another single
499+
`
500+
assert.Equal(t, expected, table.String())
501+
}
502+
503+
func TestStringTabWriterTruncateDisabled(t *testing.T) {
504+
TableConfig.UseTabWriter = true
505+
TableConfig.TabWriterTruncate = false
506+
defer func() {
507+
TableConfig.UseTabWriter = false
508+
TableConfig.TabWriterTruncate = true
509+
}()
510+
table := NewTable()
511+
table.Headers = Row{"Name", "Description"}
512+
table.AddRow(Row{"Item1", "Single line"})
513+
table.AddRow(Row{"Item2", "First line\nSecond line"})
514+
table.AddRow(Row{"Item3", "Another single"})
515+
expected := `NAME DESCRIPTION
516+
Item1 Single line
517+
Item2 First line Second line
518+
Item3 Another single
519+
`
520+
assert.Equal(t, expected, table.String())
521+
}
522+
523+
func TestStringTabWriterDisableTableTruncate(t *testing.T) {
524+
TableConfig.UseTabWriter = true
525+
TableConfig.TabWriterTruncate = true
526+
defer func() {
527+
TableConfig.UseTabWriter = false
528+
TableConfig.TabWriterTruncate = true
529+
}()
530+
table := NewTable()
531+
table.DisableTableWriterTruncate = true
532+
table.Headers = Row{"Name", "Description"}
533+
table.AddRow(Row{"Item1", "Single line"})
534+
table.AddRow(Row{"Item2", "First line\nSecond line"})
535+
expected := `NAME DESCRIPTION
536+
Item1 Single line
537+
Item2 First line Second line
538+
`
539+
assert.Equal(t, expected, table.String())
540+
}
541+
542+
func TestStringTabWriterPadding(t *testing.T) {
543+
TableConfig.UseTabWriter = true
544+
defer func() {
545+
TableConfig.UseTabWriter = false
546+
}()
547+
table := NewTable()
548+
table.TableWriterPadding = 2
549+
table.Headers = Row{"Word", "Number"}
550+
table.AddRow(Row{"One", "1"})
551+
table.AddRow(Row{"Two", "2"})
552+
expected := ` WORD NUMBER
553+
One 1
554+
Two 2
555+
`
556+
assert.Equal(t, expected, table.String())
557+
}
558+
559+
func TestStringTabWriterFormFeedAndCarriageReturn(t *testing.T) {
560+
TableConfig.UseTabWriter = true
561+
TableConfig.TabWriterTruncate = true
562+
defer func() {
563+
TableConfig.UseTabWriter = false
564+
TableConfig.TabWriterTruncate = true
565+
}()
566+
table := NewTable()
567+
table.Headers = Row{"Name", "Value"}
568+
table.AddRow(Row{"FormFeed", "before\fafter"})
569+
table.AddRow(Row{"CarriageReturn", "before\rafter"})
570+
table.AddRow(Row{"Mixed", "a\fb\nc\rd"})
571+
expected := `NAME VALUE
572+
FormFeed before ...
573+
CarriageReturn before ...
574+
Mixed a ...
575+
`
576+
assert.Equal(t, expected, table.String())
577+
}
578+
579+
func TestStringTabWriterFormFeedAndCarriageReturnNoTruncate(t *testing.T) {
580+
TableConfig.UseTabWriter = true
581+
TableConfig.TabWriterTruncate = false
582+
defer func() {
583+
TableConfig.UseTabWriter = false
584+
TableConfig.TabWriterTruncate = true
585+
}()
586+
table := NewTable()
587+
table.Headers = Row{"Name", "Value"}
588+
table.AddRow(Row{"FormFeed", "before\fafter"})
589+
table.AddRow(Row{"CarriageReturn", "before\rafter"})
590+
table.AddRow(Row{"Mixed", "a\fb\nc\rd"})
591+
expected := `NAME VALUE
592+
FormFeed before after
593+
CarriageReturn before after
594+
Mixed a b c d
482595
`
483596
assert.Equal(t, expected, table.String())
484597
}

0 commit comments

Comments
 (0)