Skip to content

Commit

Permalink
table: style option to color borders/separators
Browse files Browse the repository at this point in the history
  • Loading branch information
jedib0t committed Feb 27, 2023
1 parent 0841622 commit 1a412a0
Show file tree
Hide file tree
Showing 5 changed files with 137 additions and 65 deletions.
47 changes: 47 additions & 0 deletions table/render_test.go
Expand Up @@ -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()
Expand Down
28 changes: 15 additions & 13 deletions table/style.go
Expand Up @@ -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 (
Expand All @@ -552,114 +554,114 @@ 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},
}

// 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},
}

// 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},
}

// 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},
}

// 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},
}

// 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},
}

// 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},
}
Expand Down
28 changes: 24 additions & 4 deletions table/table.go
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
}
Expand All @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
51 changes: 51 additions & 0 deletions 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
}
48 changes: 0 additions & 48 deletions text/string.go
Expand Up @@ -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"
Expand Down

0 comments on commit 1a412a0

Please sign in to comment.