Repository: jedib0t/go-pretty (go)

Issue:
Allow border colors to be set in style
**Is your feature request related to a problem? Please describe.**

Style customization is great, but I would like a more subtle change from eg rounded to red than the full visual change that is possible now.

**Describe the solution you'd like**

It would be great leave the overall style as is (eg rounded) but change just the border colors.

**Describe alternatives you've considered**

I like the customization options, but the switch between them can be quite drastic. So considering the existing ones I want to offer some more in addition.


Pull request description:
table: style option to color borders/separators
Fixes #257.


Patch:
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"


Test patch:
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()