Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

table: style option to color borders/separators #259

Merged
merged 1 commit into from
Feb 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
47 changes: 47 additions & 0 deletions table/render_test.go
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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