From d51361b12d5654ed8a1e2b983ddfb266fdcf0a8e Mon Sep 17 00:00:00 2001 From: Naveen Mahalingam Date: Sun, 26 Feb 2023 21:11:52 -0800 Subject: [PATCH] table: style option to color borders/separators (#259) --- table/render_test.go | 47 ++++++++++++++++++++++++++++++++++++++++ table/style.go | 28 +++++++++++++----------- table/table.go | 28 ++++++++++++++++++++---- text/escape.go | 51 ++++++++++++++++++++++++++++++++++++++++++++ text/string.go | 48 ----------------------------------------- 5 files changed, 137 insertions(+), 65 deletions(-) create mode 100644 text/escape.go diff --git a/table/render_test.go b/table/render_test.go index ceb8041..81f104c 100644 --- a/table/render_test.go +++ b/table/render_test.go @@ -941,6 +941,53 @@ func TestTable_Render_BorderAndSeparators(t *testing.T) { +-----+------------+-----------+--------+-----------------------------+`) } +func TestTable_Render_BorderAndSeparators_Colored(t *testing.T) { + table := Table{} + table.AppendHeader(testHeader) + table.AppendRows(testRows) + table.AppendFooter(testFooter) + + compareOutput(t, table.Render(), ` ++-----+------------+-----------+--------+-----------------------------+ +| # | FIRST NAME | LAST NAME | SALARY | | ++-----+------------+-----------+--------+-----------------------------+ +| 1 | Arya | Stark | 3000 | | +| 20 | Jon | Snow | 2000 | You know nothing, Jon Snow! | +| 300 | Tyrion | Lannister | 5000 | | ++-----+------------+-----------+--------+-----------------------------+ +| | | TOTAL | 10000 | | ++-----+------------+-----------+--------+-----------------------------+`) + + table.Style().Color.Border = text.Colors{text.FgRed} + table.Style().Color.Separator = text.Colors{text.FgYellow} + compareOutputColored(t, table.Render(), ""+ + "\x1b[31m+\x1b[0m\x1b[31m-----\x1b[0m\x1b[31m+\x1b[0m\x1b[31m------------\x1b[0m\x1b[31m+\x1b[0m\x1b[31m-----------\x1b[0m\x1b[31m+\x1b[0m\x1b[31m--------\x1b[0m\x1b[31m+\x1b[0m\x1b[31m-----------------------------\x1b[0m\x1b[31m+\x1b[0m\n"+ + "\x1b[31m|\x1b[0m # \x1b[33m|\x1b[0m FIRST NAME \x1b[33m|\x1b[0m LAST NAME \x1b[33m|\x1b[0m SALARY \x1b[33m|\x1b[0m \x1b[31m|\x1b[0m\n"+ + "\x1b[31m+\x1b[0m\x1b[33m-----\x1b[0m\x1b[33m+\x1b[0m\x1b[33m------------\x1b[0m\x1b[33m+\x1b[0m\x1b[33m-----------\x1b[0m\x1b[33m+\x1b[0m\x1b[33m--------\x1b[0m\x1b[33m+\x1b[0m\x1b[33m-----------------------------\x1b[0m\x1b[31m+\x1b[0m\n"+ + "\x1b[31m|\x1b[0m 1 \x1b[33m|\x1b[0m Arya \x1b[33m|\x1b[0m Stark \x1b[33m|\x1b[0m 3000 \x1b[33m|\x1b[0m \x1b[31m|\x1b[0m\n"+ + "\x1b[31m|\x1b[0m 20 \x1b[33m|\x1b[0m Jon \x1b[33m|\x1b[0m Snow \x1b[33m|\x1b[0m 2000 \x1b[33m|\x1b[0m You know nothing, Jon Snow! \x1b[31m|\x1b[0m\n"+ + "\x1b[31m|\x1b[0m 300 \x1b[33m|\x1b[0m Tyrion \x1b[33m|\x1b[0m Lannister \x1b[33m|\x1b[0m 5000 \x1b[33m|\x1b[0m \x1b[31m|\x1b[0m\n"+ + "\x1b[31m+\x1b[0m\x1b[33m-----\x1b[0m\x1b[33m+\x1b[0m\x1b[33m------------\x1b[0m\x1b[33m+\x1b[0m\x1b[33m-----------\x1b[0m\x1b[33m+\x1b[0m\x1b[33m--------\x1b[0m\x1b[33m+\x1b[0m\x1b[33m-----------------------------\x1b[0m\x1b[31m+\x1b[0m\n"+ + "\x1b[31m|\x1b[0m \x1b[33m|\x1b[0m \x1b[33m|\x1b[0m TOTAL \x1b[33m|\x1b[0m 10000 \x1b[33m|\x1b[0m \x1b[31m|\x1b[0m\n"+ + "\x1b[31m+\x1b[0m\x1b[31m-----\x1b[0m\x1b[31m+\x1b[0m\x1b[31m------------\x1b[0m\x1b[31m+\x1b[0m\x1b[31m-----------\x1b[0m\x1b[31m+\x1b[0m\x1b[31m--------\x1b[0m\x1b[31m+\x1b[0m\x1b[31m-----------------------------\x1b[0m\x1b[31m+\x1b[0m", + ) + + table.SetStyle(StyleLight) + table.Style().Color.Border = text.Colors{text.FgRed} + table.Style().Color.Separator = text.Colors{text.FgYellow} + compareOutputColored(t, table.Render(), ""+ + "\x1b[31m┌\x1b[0m\x1b[31m─────\x1b[0m\x1b[31m┬\x1b[0m\x1b[31m────────────\x1b[0m\x1b[31m┬\x1b[0m\x1b[31m───────────\x1b[0m\x1b[31m┬\x1b[0m\x1b[31m────────\x1b[0m\x1b[31m┬\x1b[0m\x1b[31m─────────────────────────────\x1b[0m\x1b[31m┐\x1b[0m\n"+ + "\x1b[31m│\x1b[0m # \x1b[33m│\x1b[0m FIRST NAME \x1b[33m│\x1b[0m LAST NAME \x1b[33m│\x1b[0m SALARY \x1b[33m│\x1b[0m \x1b[31m│\x1b[0m\n"+ + "\x1b[31m├\x1b[0m\x1b[33m─────\x1b[0m\x1b[33m┼\x1b[0m\x1b[33m────────────\x1b[0m\x1b[33m┼\x1b[0m\x1b[33m───────────\x1b[0m\x1b[33m┼\x1b[0m\x1b[33m────────\x1b[0m\x1b[33m┼\x1b[0m\x1b[33m─────────────────────────────\x1b[0m\x1b[31m┤\x1b[0m\n"+ + "\x1b[31m│\x1b[0m 1 \x1b[33m│\x1b[0m Arya \x1b[33m│\x1b[0m Stark \x1b[33m│\x1b[0m 3000 \x1b[33m│\x1b[0m \x1b[31m│\x1b[0m\n"+ + "\x1b[31m│\x1b[0m 20 \x1b[33m│\x1b[0m Jon \x1b[33m│\x1b[0m Snow \x1b[33m│\x1b[0m 2000 \x1b[33m│\x1b[0m You know nothing, Jon Snow! \x1b[31m│\x1b[0m\n"+ + "\x1b[31m│\x1b[0m 300 \x1b[33m│\x1b[0m Tyrion \x1b[33m│\x1b[0m Lannister \x1b[33m│\x1b[0m 5000 \x1b[33m│\x1b[0m \x1b[31m│\x1b[0m\n"+ + "\x1b[31m├\x1b[0m\x1b[33m─────\x1b[0m\x1b[33m┼\x1b[0m\x1b[33m────────────\x1b[0m\x1b[33m┼\x1b[0m\x1b[33m───────────\x1b[0m\x1b[33m┼\x1b[0m\x1b[33m────────\x1b[0m\x1b[33m┼\x1b[0m\x1b[33m─────────────────────────────\x1b[0m\x1b[31m┤\x1b[0m\n"+ + "\x1b[31m│\x1b[0m \x1b[33m│\x1b[0m \x1b[33m│\x1b[0m TOTAL \x1b[33m│\x1b[0m 10000 \x1b[33m│\x1b[0m \x1b[31m│\x1b[0m\n"+ + "\x1b[31m└\x1b[0m\x1b[31m─────\x1b[0m\x1b[31m┴\x1b[0m\x1b[31m────────────\x1b[0m\x1b[31m┴\x1b[0m\x1b[31m───────────\x1b[0m\x1b[31m┴\x1b[0m\x1b[31m────────\x1b[0m\x1b[31m┴\x1b[0m\x1b[31m─────────────────────────────\x1b[0m\x1b[31m┘\x1b[0m", + ) +} + func TestTable_Render_Colored(t *testing.T) { t.Run("simple", func(t *testing.T) { tw := NewWriter() diff --git a/table/style.go b/table/style.go index b55b04d..ee87ef8 100644 --- a/table/style.go +++ b/table/style.go @@ -533,11 +533,13 @@ var ( // ColorOptions defines the ANSI colors to use for parts of the Table. type ColorOptions struct { - IndexColumn text.Colors // index-column colors (row #, etc.) + Border text.Colors // borders (if nil, uses one of the below) Footer text.Colors // footer row(s) colors Header text.Colors // header row(s) colors + IndexColumn text.Colors // index-column colors (row #, etc.) Row text.Colors // regular row(s) colors RowAlternate text.Colors // regular row(s) colors for the even-numbered rows + Separator text.Colors // separators (if nil, uses one of the above) } var ( @@ -552,18 +554,18 @@ var ( // ColorOptionsBlackOnBlueWhite renders Black text on Blue/White background. ColorOptionsBlackOnBlueWhite = ColorOptions{ - IndexColumn: text.Colors{text.BgHiBlue, text.FgBlack}, Footer: text.Colors{text.BgBlue, text.FgBlack}, Header: text.Colors{text.BgHiBlue, text.FgBlack}, + IndexColumn: text.Colors{text.BgHiBlue, text.FgBlack}, Row: text.Colors{text.BgHiWhite, text.FgBlack}, RowAlternate: text.Colors{text.BgWhite, text.FgBlack}, } // ColorOptionsBlackOnCyanWhite renders Black text on Cyan/White background. ColorOptionsBlackOnCyanWhite = ColorOptions{ - IndexColumn: text.Colors{text.BgHiCyan, text.FgBlack}, Footer: text.Colors{text.BgCyan, text.FgBlack}, Header: text.Colors{text.BgHiCyan, text.FgBlack}, + IndexColumn: text.Colors{text.BgHiCyan, text.FgBlack}, Row: text.Colors{text.BgHiWhite, text.FgBlack}, RowAlternate: text.Colors{text.BgWhite, text.FgBlack}, } @@ -571,9 +573,9 @@ var ( // ColorOptionsBlackOnGreenWhite renders Black text on Green/White // background. ColorOptionsBlackOnGreenWhite = ColorOptions{ - IndexColumn: text.Colors{text.BgHiGreen, text.FgBlack}, Footer: text.Colors{text.BgGreen, text.FgBlack}, Header: text.Colors{text.BgHiGreen, text.FgBlack}, + IndexColumn: text.Colors{text.BgHiGreen, text.FgBlack}, Row: text.Colors{text.BgHiWhite, text.FgBlack}, RowAlternate: text.Colors{text.BgWhite, text.FgBlack}, } @@ -581,18 +583,18 @@ var ( // ColorOptionsBlackOnMagentaWhite renders Black text on Magenta/White // background. ColorOptionsBlackOnMagentaWhite = ColorOptions{ - IndexColumn: text.Colors{text.BgHiMagenta, text.FgBlack}, Footer: text.Colors{text.BgMagenta, text.FgBlack}, Header: text.Colors{text.BgHiMagenta, text.FgBlack}, + IndexColumn: text.Colors{text.BgHiMagenta, text.FgBlack}, Row: text.Colors{text.BgHiWhite, text.FgBlack}, RowAlternate: text.Colors{text.BgWhite, text.FgBlack}, } // ColorOptionsBlackOnRedWhite renders Black text on Red/White background. ColorOptionsBlackOnRedWhite = ColorOptions{ - IndexColumn: text.Colors{text.BgHiRed, text.FgBlack}, Footer: text.Colors{text.BgRed, text.FgBlack}, Header: text.Colors{text.BgHiRed, text.FgBlack}, + IndexColumn: text.Colors{text.BgHiRed, text.FgBlack}, Row: text.Colors{text.BgHiWhite, text.FgBlack}, RowAlternate: text.Colors{text.BgWhite, text.FgBlack}, } @@ -600,27 +602,27 @@ var ( // ColorOptionsBlackOnYellowWhite renders Black text on Yellow/White // background. ColorOptionsBlackOnYellowWhite = ColorOptions{ - IndexColumn: text.Colors{text.BgHiYellow, text.FgBlack}, Footer: text.Colors{text.BgYellow, text.FgBlack}, Header: text.Colors{text.BgHiYellow, text.FgBlack}, + IndexColumn: text.Colors{text.BgHiYellow, text.FgBlack}, Row: text.Colors{text.BgHiWhite, text.FgBlack}, RowAlternate: text.Colors{text.BgWhite, text.FgBlack}, } // ColorOptionsBlueWhiteOnBlack renders Blue/White text on Black background. ColorOptionsBlueWhiteOnBlack = ColorOptions{ - IndexColumn: text.Colors{text.FgHiBlue, text.BgHiBlack}, Footer: text.Colors{text.FgBlue, text.BgHiBlack}, Header: text.Colors{text.FgHiBlue, text.BgHiBlack}, + IndexColumn: text.Colors{text.FgHiBlue, text.BgHiBlack}, Row: text.Colors{text.FgHiWhite, text.BgBlack}, RowAlternate: text.Colors{text.FgWhite, text.BgBlack}, } // ColorOptionsCyanWhiteOnBlack renders Cyan/White text on Black background. ColorOptionsCyanWhiteOnBlack = ColorOptions{ - IndexColumn: text.Colors{text.FgHiCyan, text.BgHiBlack}, Footer: text.Colors{text.FgCyan, text.BgHiBlack}, Header: text.Colors{text.FgHiCyan, text.BgHiBlack}, + IndexColumn: text.Colors{text.FgHiCyan, text.BgHiBlack}, Row: text.Colors{text.FgHiWhite, text.BgBlack}, RowAlternate: text.Colors{text.FgWhite, text.BgBlack}, } @@ -628,9 +630,9 @@ var ( // ColorOptionsGreenWhiteOnBlack renders Green/White text on Black // background. ColorOptionsGreenWhiteOnBlack = ColorOptions{ - IndexColumn: text.Colors{text.FgHiGreen, text.BgHiBlack}, Footer: text.Colors{text.FgGreen, text.BgHiBlack}, Header: text.Colors{text.FgHiGreen, text.BgHiBlack}, + IndexColumn: text.Colors{text.FgHiGreen, text.BgHiBlack}, Row: text.Colors{text.FgHiWhite, text.BgBlack}, RowAlternate: text.Colors{text.FgWhite, text.BgBlack}, } @@ -638,18 +640,18 @@ var ( // ColorOptionsMagentaWhiteOnBlack renders Magenta/White text on Black // background. ColorOptionsMagentaWhiteOnBlack = ColorOptions{ - IndexColumn: text.Colors{text.FgHiMagenta, text.BgHiBlack}, Footer: text.Colors{text.FgMagenta, text.BgHiBlack}, Header: text.Colors{text.FgHiMagenta, text.BgHiBlack}, + IndexColumn: text.Colors{text.FgHiMagenta, text.BgHiBlack}, Row: text.Colors{text.FgHiWhite, text.BgBlack}, RowAlternate: text.Colors{text.FgWhite, text.BgBlack}, } // ColorOptionsRedWhiteOnBlack renders Red/White text on Black background. ColorOptionsRedWhiteOnBlack = ColorOptions{ - IndexColumn: text.Colors{text.FgHiRed, text.BgHiBlack}, Footer: text.Colors{text.FgRed, text.BgHiBlack}, Header: text.Colors{text.FgHiRed, text.BgHiBlack}, + IndexColumn: text.Colors{text.FgHiRed, text.BgHiBlack}, Row: text.Colors{text.FgHiWhite, text.BgBlack}, RowAlternate: text.Colors{text.FgWhite, text.BgBlack}, } @@ -657,9 +659,9 @@ var ( // ColorOptionsYellowWhiteOnBlack renders Yellow/White text on Black // background. ColorOptionsYellowWhiteOnBlack = ColorOptions{ - IndexColumn: text.Colors{text.FgHiYellow, text.BgHiBlack}, Footer: text.Colors{text.FgYellow, text.BgHiBlack}, Header: text.Colors{text.FgHiYellow, text.BgHiBlack}, + IndexColumn: text.Colors{text.FgHiYellow, text.BgHiBlack}, Row: text.Colors{text.FgHiWhite, text.BgBlack}, RowAlternate: text.Colors{text.FgWhite, text.BgBlack}, } diff --git a/table/table.go b/table/table.go index 2369199..7b19dcc 100644 --- a/table/table.go +++ b/table/table.go @@ -328,6 +328,8 @@ func (t *Table) getAutoIndexColumnIDs() rowStr { func (t *Table) getBorderColors(hint renderHint) text.Colors { if t.style.Options.DoNotColorBordersAndSeparators { return nil + } else if t.style.Color.Border != nil { + return t.style.Color.Border } else if hint.isHeaderRow { return t.style.Color.Header } else if hint.isFooterRow { @@ -383,12 +385,13 @@ func (t *Table) getBorderRight(hint renderHint) string { } func (t *Table) getColumnColors(colIdx int, hint renderHint) text.Colors { - if hint.isBorderOrSeparator() && t.style.Options.DoNotColorBordersAndSeparators { - return text.Colors{} // not nil to force caller to paint with no colors + if hint.isBorderOrSeparator() { + if colors := t.getColumnColorsForBorderOrSeparator(colIdx, hint); colors != nil { + return colors + } } if t.rowPainter != nil && hint.isRegularNonSeparatorRow() && !t.isIndexColumn(colIdx, hint) { - colors := t.rowsColors[hint.rowNumber-1] - if colors != nil { + if colors := t.rowsColors[hint.rowNumber-1]; colors != nil { return colors } } @@ -405,6 +408,19 @@ func (t *Table) getColumnColors(colIdx int, hint renderHint) text.Colors { return nil } +func (t *Table) getColumnColorsForBorderOrSeparator(colIdx int, hint renderHint) text.Colors { + if t.style.Options.DoNotColorBordersAndSeparators { + return text.Colors{} // not nil to force caller to paint with no colors + } + if (hint.isBorderBottom || hint.isBorderTop) && t.style.Color.Border != nil { + return t.style.Color.Border + } + if hint.isSeparatorRow && t.style.Color.Separator != nil { + return t.style.Color.Separator + } + return nil +} + func (t *Table) getColumnSeparator(row rowStr, colIdx int, hint renderHint) string { separator := t.style.Box.MiddleVertical if hint.isSeparatorRow { @@ -585,6 +601,10 @@ func (t *Table) getRowConfig(hint renderHint) RowConfig { func (t *Table) getSeparatorColors(hint renderHint) text.Colors { if t.style.Options.DoNotColorBordersAndSeparators { return nil + } else if (hint.isBorderBottom || hint.isBorderTop) && t.style.Color.Border != nil { + return t.style.Color.Border + } else if t.style.Color.Separator != nil { + return t.style.Color.Separator } else if hint.isHeaderRow { return t.style.Color.Header } else if hint.isFooterRow { diff --git a/text/escape.go b/text/escape.go new file mode 100644 index 0000000..d54e66e --- /dev/null +++ b/text/escape.go @@ -0,0 +1,51 @@ +package text + +import "strings" + +// Constants +const ( + CSIStartRune = rune(91) // [ + CSIStopRune = 'm' + EscapeReset = EscapeStart + "0" + EscapeStop + EscapeStart = "\x1b[" + EscapeStartRune = rune(27) // \x1b + EscapeStop = "m" + EscapeStopRune = 'm' + OSIStartRune = rune(93) // ] + OSIStopRune = '\\' +) + +type escKind int + +const ( + escKindUnknown escKind = iota + escKindCSI + escKindOSI +) + +type escSeq struct { + isIn bool + content strings.Builder + kind escKind +} + +func (e *escSeq) InspectRune(r rune) { + if !e.isIn && r == EscapeStartRune { + e.isIn = true + e.kind = escKindUnknown + e.content.Reset() + e.content.WriteRune(r) + } else if e.isIn { + switch { + case e.kind == escKindUnknown && r == CSIStartRune: + e.kind = escKindCSI + case e.kind == escKindUnknown && r == OSIStartRune: + e.kind = escKindOSI + case e.kind == escKindCSI && r == CSIStopRune || e.kind == escKindOSI && r == OSIStopRune: + e.isIn = false + e.kind = escKindUnknown + } + e.content.WriteRune(r) + } + return +} diff --git a/text/string.go b/text/string.go index cd672ab..6a21dda 100644 --- a/text/string.go +++ b/text/string.go @@ -7,59 +7,11 @@ import ( "github.com/mattn/go-runewidth" ) -// Constants -const ( - EscapeReset = EscapeStart + "0" + EscapeStop - EscapeStart = "\x1b[" - CSIStartRune = rune(91) // [ - CSIStopRune = 'm' - OSIStartRune = rune(93) // ] - OSIStopRune = '\\' - EscapeStartRune = rune(27) // \x1b - EscapeStop = "m" - EscapeStopRune = 'm' -) - // RuneWidth stuff var ( rwCondition = runewidth.NewCondition() ) -type escKind int - -const ( - Unknown escKind = iota - CSI - OSI -) - -type escSeq struct { - isIn bool - content strings.Builder - kind escKind -} - -func (e *escSeq) InspectRune(r rune) { - if !e.isIn && r == EscapeStartRune { - e.isIn = true - e.kind = Unknown - e.content.Reset() - e.content.WriteRune(r) - } else if e.isIn { - switch { - case e.kind == Unknown && r == CSIStartRune: - e.kind = CSI - case e.kind == Unknown && r == OSIStartRune: - e.kind = OSI - case e.kind == CSI && r == CSIStopRune || e.kind == OSI && r == OSIStopRune: - e.isIn = false - e.kind = Unknown - } - e.content.WriteRune(r) - } - return -} - // InsertEveryN inserts the rune every N characters in the string. For ex.: // InsertEveryN("Ghost", '-', 1) == "G-h-o-s-t" // InsertEveryN("Ghost", '-', 2) == "Gh-os-t"