Repository: pterm/pterm
Pull request #667 (2024-07-25 09:01:41)

Add alternate style for table
### Description
Add colored rows intermittently in tables with `.WithAlternateRowStyle(pterm.NewStyle(pterm.BgDarkGray))`

### Scope
table/alternate-row-style

- [ ] Bug Fix
- [X] New Feature
- [ ] Documentation
- [ ] Other

### Related Issue
Fixes https://github.com/pterm/pterm/issues/666

To-Do Checklist
- [X] tested my changes
- [X] have commented every method that I created/changed
- [X] updated the examples to fit with my changes
- [X] have added tests for my newly created methods

It's my first PR here, i don't know if i made all things correctly













MarvinJWendt: Thanks for the PR! 
Also thanks for fixing the linting, even tho the warnings were out of scope of your feature 💙 

Patch:
diff --git a/_examples/table/README.md b/_examples/table/README.md
index 32f32131..0f893961 100644
--- a/_examples/table/README.md
+++ b/_examples/table/README.md
@@ -146,3 +146,39 @@ func main() {
 
 </details>
 
+### table/alternate-row-style
+
+![Animation](https://raw.githubusercontent.com/pterm/pterm/master/_examples/table/alternate-row-style/animation.svg)
+
+<details>
+
+<summary>SHOW SOURCE</summary>
+
+```go
+package main
+
+import "github.com/pterm/pterm"
+
+func main() {
+	// Define the data for the table.
+	// Each inner slice represents a row in the table.
+	// The first row is considered as the header of the table.
+	alternateStyle := pterm.NewStyle(pterm.BgDarkGray)
+
+	tableData := pterm.TableData{
+		{"Firstname", "Lastname", "Email", "Note"},
+		{"Paul", "Dean", "augue@velitAliquam.co.uk", ""},
+		{"Callie", "Mckay", "nunc.sed@est.com", "这是一个测试, haha!"},
+		{"Libby", "Camacho", "lobortis@semper.com", "just a test, hey!"},
+		{"张", "小宝", "zhang@example.com", ""},
+	}
+
+	// Create a table with the defined data.
+	// The table has a header and is boxed.
+	// Finally, render the table to print it.
+	pterm.DefaultTable.WithHasHeader().WithBoxed().WithData(tableData).WithAlternateRowStyle(alternateStyle).Render()
+}
+```
+
+</details>
+
diff --git a/_examples/table/alternate-row-style/README.md b/_examples/table/alternate-row-style/README.md
new file mode 100644
index 00000000..67a88717
--- /dev/null
+++ b/_examples/table/alternate-row-style/README.md
@@ -0,0 +1,33 @@
+# table/boxed
+
+![Animation](animation.svg)
+
+```go
+package main
+
+import "github.com/pterm/pterm"
+
+func main() {
+	// Define the data for the table.
+	// Each inner slice represents a row in the table.
+	// The first row is considered as the header of the table.
+
+	tableData := pterm.TableData{
+		{"Firstname", "Lastname", "Email", "Note"},
+		{"Paul", "Dean", "augue@velitAliquam.co.uk", ""},
+		{"Callie", "Mckay", "nunc.sed@est.com", "这是一个测试, haha!"},
+		{"Libby", "Camacho", "lobortis@semper.com", "just a test, hey!"},
+		{"张", "小宝", "zhang@example.com", ""},
+	}
+
+	// Create an alternate color style for the rows.
+	alternateStyle := pterm.NewStyle(pterm.BgDarkGray)
+
+	// Create a table with the defined data.
+	// The table has a header and is boxed.
+	// Colors are used to alternate the rows.
+	// Finally, render the table to print it.
+	pterm.DefaultTable.WithHasHeader().WithBoxed().WithData(tableData).WithAlternateRowStyle(alternateStyle).Render()
+}
+
+```
diff --git a/_examples/table/alternate-row-style/animation.svg b/_examples/table/alternate-row-style/animation.svg
new file mode 100644
index 00000000..411eea9a
--- /dev/null
+++ b/_examples/table/alternate-row-style/animation.svg
@@ -0,0 +1,10 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="840" height="581.04"><rect width="840" height="581.04" rx="5" ry="5" class="a"></rect><svg y="0%" x="0%"><circle cx="20" cy="20" r="6" class="b"></circle><circle cx="40" cy="20" r="6" class="c"></circle><circle cx="60" cy="20" r="6" class="d"></circle></svg><svg height="521.04" viewBox="0 0 80 52.104" width="800" x="15" y="50" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><style>.e, .g, .f {
+    font-family: 
+            'Courier New', 
+            Roboto, 
+            Helvetica, 
+            Arial, 
+            sans-serif, 
+            'Apple Color Emoji', 
+            'Segoe UI Emoji' !important;
+}.a {fill: rgb(35, 35, 35);}.b {fill: #ff5f58;}.c {fill: #ffbd2e;}.d {fill: #18c132;}.e {font-family: 'Courier New', Monaco, Consolas, Menlo, 'Bitstream Vera Sans Mono', 'Powerline Symbols', monospace;}.f {fill: rgb(160, 160, 160);white-space: pre;}.g {fill: rgb(126, 231, 233);white-space: pre;}.h {fill: rgb(104, 104, 104);white-space: pre;}.i {fill: transparent;}.j {fill: rgb(50, 50, 50);white-space: pre;}.k {animation-duration: 5.221808s;animation-iteration-count: infinite;animation-name: l;animation-timing-function: steps(1, end);}@keyframes l {0% {transform: translateX(-0px);}1.5% {transform: translateX(-80px);}4.2% {transform: translateX(-160px);}4.2% {transform: translateX(-240px);}100% {transform: translateX(-320px);}}</style><g font-family="'Courier New', Monaco, Consolas, Menlo, 'Bitstream Vera Sans Mono', 'Powerline Symbols', monospace" font-size="1.67" class="e"><defs><symbol id="1"><text x="0" y="1.67" class="f">┌───────────────────────────────────────────────────────────────────────┐</text></symbol><symbol id="2"><text x="0" y="1.67" class="f">|</text><text x="2.004" y="1.67" class="g">Firstname</text><text x="12.024" y="1.67" class="h">|</text><text x="14.028" y="1.67" class="g">Lastname</text><text x="23.046" y="1.67" class="h">|</text><text x="25.05" y="1.67" class="g">Email</text><text x="50.1" y="1.67" class="h">|</text><text x="52.104" y="1.67" class="g">Note</text><text x="72.144" y="1.67" class="f">|</text></symbol><symbol id="3"><rect width="80" height="2.171" y="0" class="i"></rect><text x="0" y="1.67" class="f">|</text><text x="2.004" y="1.67" class="f">Paul</text><text x="12.024" y="1.67" class="h">|</text><text x="14.028" y="1.67" class="f">Dean</text><text x="23.046" y="1.67" class="h">|</text><text x="25.05" y="1.67" class="f">augue@velitAliquam.co.uk</text><text x="50.1" y="1.67" class="h">|</text><text x="72.144" y="1.67" class="f">|</text></symbol><symbol id="4"><rect width="72.144" height="2.171" y="0" class="j"></rect><text x="0" y="1.67" class="f">|</text><text x="2.004" y="1.67" class="f">Callie</text><text x="12.024" y="1.67" class="h">|</text><text x="14.028" y="1.67" class="f">Mckay</text><text x="23.046" y="1.67" class="h">|</text><text x="25.05" y="1.67" class="f">nunc.sed@est.com</text><text x="50.1" y="1.67" class="h">|</text><text x="52.104" y="1.67" class="f">这是一个测试,</text><text x="60.12" y="1.67" class="f">haha!</text><text x="72.144" y="1.67" class="f">|</text></symbol><symbol id="5"><rect width="80" height="2.171" y="0" class="i"></rect><text x="0" y="1.67" class="f">|</text><text x="2.004" y="1.67" class="f">Libby</text><text x="12.024" y="1.67" class="h">|</text><text x="14.028" y="1.67" class="f">Camacho</text><text x="23.046" y="1.67" class="h">|</text><text x="25.05" y="1.67" class="f">lobortis@semper.com</text><text x="50.1" y="1.67" class="h">|</text><text x="52.104" y="1.67" class="f">just</text><text x="57.114" y="1.67" class="f">a</text><text x="59.118" y="1.67" class="f">test,</text><text x="65.13" y="1.67" class="f">hey!</text><text x="72.144" y="1.67" class="f">|</text></symbol><symbol id="6"><rect width="72.144" height="2.171" y="0" class="j"></rect><text x="0" y="1.67" class="f">|</text><text x="2.004" y="1.67" class="f">张</text><text x="12.024" y="1.67" class="h">|</text><text x="14.028" y="1.67" class="f">小宝</text><text x="23.046" y="1.67" class="h">|</text><text x="25.05" y="1.67" class="f">zhang@example.com</text><text x="50.1" y="1.67" class="h">|</text><text x="72.144" y="1.67" class="f">|</text></symbol><symbol id="7"><text x="0" y="1.67" class="f">└───────────────────────────────────────────────────────────────────────┘</text></symbol><symbol id="a"><rect height="24" width="80" x="0" y="0" class="i"></rect></symbol></defs><rect height="52.104" width="80" class="a"></rect><g class="k"><svg x="0" y="0" width="400"><svg x="0"><use xlink:href="#a"></use></svg><svg x="80"><use xlink:href="#a"></use><use xlink:href="#1" y="0"></use><use xlink:href="#2" y="2.171"></use><use xlink:href="#3" y="4.342"></use><use xlink:href="#4" y="6.513"></use></svg><svg x="160"><use xlink:href="#a"></use><use xlink:href="#1" y="0"></use><use xlink:href="#2" y="2.171"></use><use xlink:href="#3" y="4.342"></use><use xlink:href="#4" y="6.513"></use></svg><svg x="240"><use xlink:href="#a"></use><use xlink:href="#1" y="0"></use><use xlink:href="#2" y="2.171"></use><use xlink:href="#3" y="4.342"></use><use xlink:href="#4" y="6.513"></use><use xlink:href="#5" y="8.684"></use><use xlink:href="#6" y="10.855"></use><use xlink:href="#7" y="13.026"></use></svg><svg x="320"><use xlink:href="#a"></use><use xlink:href="#1" y="0"></use><use xlink:href="#2" y="2.171"></use><use xlink:href="#3" y="4.342"></use><use xlink:href="#4" y="6.513"></use><use xlink:href="#5" y="8.684"></use><use xlink:href="#6" y="10.855"></use><use xlink:href="#7" y="13.026"></use><text x="0" y="19.038" class="f">Restarting</text><text x="11.022" y="19.038" class="f">animation...</text></svg></svg></g></g></svg></svg>
diff --git a/_examples/table/alternate-row-style/main.go b/_examples/table/alternate-row-style/main.go
new file mode 100644
index 00000000..5489745d
--- /dev/null
+++ b/_examples/table/alternate-row-style/main.go
@@ -0,0 +1,23 @@
+package main
+
+import "github.com/pterm/pterm"
+
+func main() {
+	// Define the data for the table.
+	// Each inner slice represents a row in the table.
+	// The first row is considered as the header of the table.
+	alternateStyle := pterm.NewStyle(pterm.BgDarkGray)
+
+	tableData := pterm.TableData{
+		{"Firstname", "Lastname", "Email", "Note"},
+		{"Paul", "Dean", "augue@velitAliquam.co.uk", ""},
+		{"Callie", "Mckay", "nunc.sed@est.com", "这是一个测试, haha!"},
+		{"Libby", "Camacho", "lobortis@semper.com", "just a test, hey!"},
+		{"张", "小宝", "zhang@example.com", ""},
+	}
+
+	// Create a table with the defined data.
+	// The table has a header and is boxed.
+	// Finally, render the table to print it.
+	pterm.DefaultTable.WithHasHeader().WithBoxed().WithData(tableData).WithAlternateRowStyle(alternateStyle).Render()
+}
diff --git a/heatmap_printer.go b/heatmap_printer.go
index 4fac71e3..b8e0bfad 100644
--- a/heatmap_printer.go
+++ b/heatmap_printer.go
@@ -653,16 +653,16 @@ func centerAndShorten(f float32, lineLength int, onlyColor bool) string {
 	return ct
 }
 
-func getColor(min float32, max float32, current float32, colors ...Color) Color {
+func getColor(minStep float32, maxStep float32, current float32, colors ...Color) Color {
 	// split the range into equal parts
 	// and assign a color to each part
 	// the last color is assigned to the max value
 	// and the first color to the min value
 	// the rest of the colors are assigned to the
 	// middle values
-	step := (max - min) / float32(len(colors))
+	step := (maxStep - minStep) / float32(len(colors))
 	for i := range colors {
-		if current >= min+float32(i)*step && current < min+float32(i+1)*step {
+		if current >= minStep+float32(i)*step && current < minStep+float32(i+1)*step {
 			return colors[i]
 		}
 	}
@@ -726,19 +726,19 @@ func (p HeatmapPrinter) errCheck() error {
 
 // return min and max value of a slice
 func minMaxFloat32(s [][]float32) (float32, float32) {
-	var min, max float32
-	min = math.MaxFloat32
-	max = -math.MaxFloat32
+	var minslice, maxslice float32
+	minslice = math.MaxFloat32
+	maxslice = -math.MaxFloat32
 
 	for _, r := range s {
 		for _, c := range r {
-			if c < min {
-				min = c
+			if c < minslice {
+				minslice = c
 			}
-			if c > max {
-				max = c
+			if c > maxslice {
+				maxslice = c
 			}
 		}
 	}
-	return min, max
+	return minslice, maxslice
 }
diff --git a/internal/max_text_width.go b/internal/max_text_width.go
index 1d4f0ea3..9612fcf5 100644
--- a/internal/max_text_width.go
+++ b/internal/max_text_width.go
@@ -1,20 +1,21 @@
 package internal
 
 import (
+	"strings"
+
 	"github.com/gookit/color"
 	"github.com/mattn/go-runewidth"
-	"strings"
 )
 
 // GetStringMaxWidth returns the maximum width of a string with multiple lines.
 func GetStringMaxWidth(s string) int {
-	var max int
+	var maxString int
 	ss := strings.Split(s, "\n")
 	for _, s2 := range ss {
 		s2WithoutColor := color.ClearCode(s2)
-		if runewidth.StringWidth(s2WithoutColor) > max {
-			max = runewidth.StringWidth(s2WithoutColor)
+		if runewidth.StringWidth(s2WithoutColor) > maxString {
+			maxString = runewidth.StringWidth(s2WithoutColor)
 		}
 	}
-	return max
+	return maxString
 }
diff --git a/rgb.go b/rgb.go
index 7c00835a..d1349bc1 100644
--- a/rgb.go
+++ b/rgb.go
@@ -162,32 +162,32 @@ func NewRGB(r, g, b uint8, background ...bool) RGB {
 }
 
 // Fade fades one RGB value (over other RGB values) to another RGB value, by giving the function a minimum, maximum and current value.
-func (p RGB) Fade(min, max, current float32, end ...RGB) RGB {
-	if max == current {
+func (p RGB) Fade(minRGB, maxRGB, current float32, end ...RGB) RGB {
+	if maxRGB == current {
 		return end[len(end)-1]
 	}
-	if min < 0 {
-		max -= min
-		current -= min
-		min = 0
+	if minRGB < 0 {
+		maxRGB -= minRGB
+		current -= minRGB
+		minRGB = 0
 	}
 	if len(end) == 1 {
 		return RGB{
-			R:          uint8(internal.MapRangeToRange(min, max, float32(p.R), float32(end[0].R), current)),
-			G:          uint8(internal.MapRangeToRange(min, max, float32(p.G), float32(end[0].G), current)),
-			B:          uint8(internal.MapRangeToRange(min, max, float32(p.B), float32(end[0].B), current)),
+			R:          uint8(internal.MapRangeToRange(minRGB, maxRGB, float32(p.R), float32(end[0].R), current)),
+			G:          uint8(internal.MapRangeToRange(minRGB, maxRGB, float32(p.G), float32(end[0].G), current)),
+			B:          uint8(internal.MapRangeToRange(minRGB, maxRGB, float32(p.B), float32(end[0].B), current)),
 			Background: p.Background,
 		}
 	} else if len(end) > 1 {
-		f := (max - min) / float32(len(end))
+		f := (maxRGB - minRGB) / float32(len(end))
 		tempCurrent := current
 		if f > current {
-			return p.Fade(min, f, current, end[0])
+			return p.Fade(minRGB, f, current, end[0])
 		} else {
 			for i := 0; i < len(end)-1; i++ {
 				tempCurrent -= f
 				if f > tempCurrent {
-					return end[i].Fade(min, min+f, tempCurrent, end[i+1])
+					return end[i].Fade(minRGB, minRGB+f, tempCurrent, end[i+1])
 				}
 			}
 		}
diff --git a/table_printer.go b/table_printer.go
index 54e8965a..6a698f39 100644
--- a/table_printer.go
+++ b/table_printer.go
@@ -41,6 +41,7 @@ type TablePrinter struct {
 	LeftAlignment           bool
 	RightAlignment          bool
 	Writer                  io.Writer
+	AlternateRowStyle       *Style
 }
 
 // WithStyle returns a new TablePrinter with a specific Style.
@@ -103,7 +104,7 @@ func (p TablePrinter) WithData(data [][]string) *TablePrinter {
 	return &p
 }
 
-// WithCSVReader return a new TablePrinter with specified Data extracted from CSV.
+// WithCSVReader returns a new TablePrinter with specified Data extracted from CSV.
 func (p TablePrinter) WithCSVReader(reader *csv.Reader) *TablePrinter {
 	if records, err := reader.ReadAll(); err == nil {
 		p.Data = records
@@ -139,15 +140,20 @@ func (p TablePrinter) WithWriter(writer io.Writer) *TablePrinter {
 	return &p
 }
 
+// WithAlternateRowStyle returns a new TablePrinter with a specific AlternateRowStyle.
+func (p TablePrinter) WithAlternateRowStyle(style *Style) *TablePrinter {
+	p.AlternateRowStyle = style
+	return &p
+}
+
 type table struct {
 	rows            []row
 	maxColumnWidths []int
 }
 
 type row struct {
-	height       int
-	cells        []cell
-	columnWidths []int
+	height int
+	cells  []cell
 }
 
 type cell struct {
@@ -208,7 +214,7 @@ func (p TablePrinter) Srender() (string, error) {
 
 	var maxRowWidth int
 	for _, r := range t.rows {
-		rowWidth := internal.GetStringMaxWidth(p.renderRow(t, r))
+		rowWidth := internal.GetStringMaxWidth(p.renderRow(t, r, 0))
 		if rowWidth > maxRowWidth {
 			maxRowWidth = rowWidth
 		}
@@ -219,7 +225,7 @@ func (p TablePrinter) Srender() (string, error) {
 
 	for i, r := range t.rows {
 		if i == 0 && p.HasHeader {
-			s += p.HeaderStyle.Sprint(p.renderRow(t, r))
+			s += p.HeaderStyle.Sprint(p.renderRow(t, r, i))
 
 			if p.HeaderRowSeparator != "" {
 				s += strings.Repeat(p.HeaderRowSeparatorStyle.Sprint(p.HeaderRowSeparator), maxRowWidth) + "\n"
@@ -227,7 +233,7 @@ func (p TablePrinter) Srender() (string, error) {
 			continue
 		}
 
-		s += p.renderRow(t, r)
+		s += p.renderRow(t, r, i)
 
 		if p.RowSeparator != "" {
 			s += strings.Repeat(p.RowSeparatorStyle.Sprint(p.RowSeparator), maxRowWidth) + "\n"
@@ -244,8 +250,16 @@ func (p TablePrinter) Srender() (string, error) {
 // renderRow renders a row.
 // It merges the cells of a row into one string.
 // Each line of each cell is merged with the same line of the other cells.
-func (p TablePrinter) renderRow(t table, r row) string {
+func (p TablePrinter) renderRow(t table, r row, rowIndex int) string {
 	var s string
+	var currentStyle *Style
+
+	// Identify if line should use alternate style
+	if rowIndex%2 == 1 && p.AlternateRowStyle != nil {
+		currentStyle = p.AlternateRowStyle
+	} else {
+		currentStyle = p.Style
+	}
 
 	// merge lines of cells and add separator
 	// use the t.maxColumnWidths to add padding to the corresponding cell
@@ -273,9 +287,13 @@ func (p TablePrinter) renderRow(t table, r row) string {
 				s += p.SeparatorStyle.Sprint(p.Separator)
 			}
 		}
+		s += strings.Repeat(" ", t.maxColumnWidths[len(r.cells)-1]-internal.GetStringMaxWidth(r.cells[len(r.cells)-1].lines[i]))
 		s += "\n"
 	}
 
+	if currentStyle != nil {
+		return currentStyle.Sprint(s)
+	}
 	return s
 }
 


Test patch:
diff --git a/table_printer_test.go b/table_printer_test.go
index 3434ef61..bd3a88a6 100644
--- a/table_printer_test.go
+++ b/table_printer_test.go
@@ -37,6 +37,24 @@ func TestTablePrinter_Render(t *testing.T) {
 	testza.AssertNotNil(t, content)
 }
 
+func TestTablePrinterWithAlternateStyle_Render(t *testing.T) {
+	d := pterm.TableData{
+		{"Firstname", "Lastname", "Email"},
+		{"Paul", "Dean", "nisi.dictum.augue@velitAliquam.co.uk"},
+		{"Callie", "Mckay", "egestas.nunc.sed@est.com"},
+		{"Libby", "Camacho", "aliquet.lobortis@semper.com"},
+	}
+
+	// Define the alternate row style
+	alternateStyle := pterm.NewStyle(pterm.BgDarkGray)
+
+	// Create a printer with the alternate row style
+	printer := pterm.DefaultTable.WithHasHeader().WithAlternateRowStyle(alternateStyle).WithData(d)
+	content, err := printer.Srender()
+	testza.AssertNoError(t, err)
+	testza.AssertNotNil(t, content)
+}
+
 func TestTablePrinterWithRowSeparators_Render(t *testing.T) {
 	d := pterm.TableData{
 		{"Firstname", "Lastname", "Email"},