Repository: jedib0t/go-pretty
Pull request #331 (2024-10-02 20:37:17)

table: Pager to page through the output
## Proposed Changes
  - addresses requirement proposed by #325 in a different way



sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=jedib0t_go-pretty&pullRequest=331) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=jedib0t_go-pretty&pullRequest=331&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=jedib0t_go-pretty&pullRequest=331&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=jedib0t_go-pretty&pullRequest=331&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=jedib0t_go-pretty&pullRequest=331&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=jedib0t_go-pretty&pullRequest=331&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=jedib0t_go-pretty&pullRequest=331)



Patch:
diff --git a/table/pager.go b/table/pager.go
new file mode 100644
index 0000000..f4585b2
--- /dev/null
+++ b/table/pager.go
@@ -0,0 +1,70 @@
+package table
+
+import (
+	"io"
+)
+
+// Pager lets you interact with the table rendering in a paged manner.
+type Pager interface {
+	// GoTo moves to the given 1-indexed page number.
+	GoTo(pageNum int) string
+	// Location returns the current page number in 1-indexed form.
+	Location() int
+	// Next moves to the next available page and returns the same.
+	Next() string
+	// Prev moves to the previous available page and returns the same.
+	Prev() string
+	// Render returns the current page.
+	Render() string
+	// SetOutputMirror sets up the writer to which Render() will write the
+	// output other than returning.
+	SetOutputMirror(mirror io.Writer)
+}
+
+type pager struct {
+	index        int // 0-indexed
+	pages        []string
+	outputMirror io.Writer
+	size         int
+}
+
+func (p *pager) GoTo(pageNum int) string {
+	if pageNum < 1 {
+		pageNum = 1
+	}
+	if pageNum > len(p.pages) {
+		pageNum = len(p.pages)
+	}
+	p.index = pageNum - 1
+	return p.pages[p.index]
+}
+
+func (p *pager) Location() int {
+	return p.index + 1
+}
+
+func (p *pager) Next() string {
+	if p.index < len(p.pages)-1 {
+		p.index++
+	}
+	return p.pages[p.index]
+}
+
+func (p *pager) Prev() string {
+	if p.index > 0 {
+		p.index--
+	}
+	return p.pages[p.index]
+}
+
+func (p *pager) Render() string {
+	pageToWrite := p.pages[p.index]
+	if p.outputMirror != nil {
+		_, _ = p.outputMirror.Write([]byte(pageToWrite))
+	}
+	return pageToWrite
+}
+
+func (p *pager) SetOutputMirror(mirror io.Writer) {
+	p.outputMirror = mirror
+}
diff --git a/table/pager_options.go b/table/pager_options.go
new file mode 100644
index 0000000..f46ebc1
--- /dev/null
+++ b/table/pager_options.go
@@ -0,0 +1,11 @@
+package table
+
+// PagerOption helps control Paging.
+type PagerOption func(t *Table)
+
+// PageSize sets the size of each page rendered.
+func PageSize(pageSize int) PagerOption {
+	return func(t *Table) {
+		t.pager.size = pageSize
+	}
+}
diff --git a/table/render.go b/table/render.go
index 428dffe..bb883e9 100644
--- a/table/render.go
+++ b/table/render.go
@@ -214,7 +214,7 @@ func (t *Table) renderLine(out *strings.Builder, row rowStr, hint renderHint) {
 	// the header all over again with a spacing line
 	if hint.isRegularNonSeparatorRow() {
 		t.numLinesRendered++
-		if t.pageSize > 0 && t.numLinesRendered%t.pageSize == 0 && !hint.isLastLineOfLastRow() {
+		if t.pager.size > 0 && t.numLinesRendered%t.pager.size == 0 && !hint.isLastLineOfLastRow() {
 			t.renderRowsFooter(out)
 			t.renderRowsBorderBottom(out)
 			out.WriteString(t.style.Box.PageSeparator)
diff --git a/table/table.go b/table/table.go
index 6776333..8081dfb 100644
--- a/table/table.go
+++ b/table/table.go
@@ -4,6 +4,7 @@ import (
 	"fmt"
 	"io"
 	"strings"
+	"time"
 	"unicode"
 
 	"github.com/jedib0t/go-pretty/v6/text"
@@ -68,10 +69,8 @@ type Table struct {
 	numLinesRendered int
 	// outputMirror stores an io.Writer where the "Render" functions would write
 	outputMirror io.Writer
-	// pageSize stores the maximum lines to render before rendering the header
-	// again (to denote a page break) - useful when you are dealing with really
-	// long tables
-	pageSize int
+	// pager controls how the output is separated into pages
+	pager pager
 	// rows stores the rows that make up the body (in string form)
 	rows []rowStr
 	// rowsColors stores the text.Colors over-rides for each row as defined by
@@ -109,8 +108,8 @@ type Table struct {
 	// suppressEmptyColumns hides columns which have no content on all regular
 	// rows
 	suppressEmptyColumns bool
-	// supressTrailingSpaces removes all trailing spaces from the end of the last column
-	supressTrailingSpaces bool
+	// suppressTrailingSpaces removes all trailing spaces from the end of the last column
+	suppressTrailingSpaces bool
 	// title contains the text to appear above the table
 	title string
 }
@@ -192,6 +191,32 @@ func (t *Table) Length() int {
 	return len(t.rowsRaw)
 }
 
+// Pager returns an object that splits the table output into pages and
+// lets you move back and forth through them.
+func (t *Table) Pager(opts ...PagerOption) Pager {
+	for _, opt := range opts {
+		opt(t)
+	}
+
+	// use a temporary page separator for splitting up the pages
+	tempPageSep := fmt.Sprintf("%p // page separator // %d", t.rows, time.Now().UnixNano())
+
+	// backup
+	origOutputMirror, origPageSep := t.outputMirror, t.Style().Box.PageSeparator
+	// restore on exit
+	defer func() {
+		t.outputMirror = origOutputMirror
+		t.Style().Box.PageSeparator = origPageSep
+	}()
+	// override
+	t.outputMirror = nil
+	t.Style().Box.PageSeparator = tempPageSep
+	// render
+	t.pager.pages = strings.Split(t.Render(), tempPageSep)
+
+	return &t.pager
+}
+
 // ResetFooters resets and clears all the Footer rows appended earlier.
 func (t *Table) ResetFooters() {
 	t.rowsFooterRaw = nil
@@ -252,6 +277,7 @@ func (t *Table) SetIndexColumn(colNum int) {
 // in addition to returning a string.
 func (t *Table) SetOutputMirror(mirror io.Writer) {
 	t.outputMirror = mirror
+	t.pager.SetOutputMirror(mirror)
 }
 
 // SetPageSize sets the maximum number of lines to render before rendering the
@@ -259,7 +285,7 @@ func (t *Table) SetOutputMirror(mirror io.Writer) {
 // long list of rows that can span pages. Please note that the pagination logic
 // will not consider Header/Footer lines for paging.
 func (t *Table) SetPageSize(numLines int) {
-	t.pageSize = numLines
+	t.pager.size = numLines
 }
 
 // SetRowPainter sets the RowPainter function which determines the colors to use
@@ -304,7 +330,7 @@ func (t *Table) SuppressEmptyColumns() {
 
 // SuppressTrailingSpaces removes all trailing spaces from the output.
 func (t *Table) SuppressTrailingSpaces() {
-	t.supressTrailingSpaces = true
+	t.suppressTrailingSpaces = true
 }
 
 func (t *Table) getAlign(colIdx int, hint renderHint) text.Align {
@@ -689,7 +715,7 @@ func (t *Table) isIndexColumn(colIdx int, hint renderHint) bool {
 
 func (t *Table) render(out *strings.Builder) string {
 	outStr := out.String()
-	if t.supressTrailingSpaces {
+	if t.suppressTrailingSpaces {
 		var trimmed []string
 		for _, line := range strings.Split(outStr, "\n") {
 			trimmed = append(trimmed, strings.TrimRightFunc(line, unicode.IsSpace))
@@ -786,8 +812,8 @@ func (t *Table) shouldSeparateRows(rowIdx int, numRows int) bool {
 	}
 
 	pageSize := numRows
-	if t.pageSize > 0 {
-		pageSize = t.pageSize
+	if t.pager.size > 0 {
+		pageSize = t.pager.size
 	}
 	if rowIdx%pageSize == pageSize-1 { // last row of page
 		return false
diff --git a/table/writer.go b/table/writer.go
index f993d86..f5642aa 100644
--- a/table/writer.go
+++ b/table/writer.go
@@ -12,6 +12,7 @@ type Writer interface {
 	AppendRows(rows []Row, configs ...RowConfig)
 	AppendSeparator()
 	Length() int
+	Pager(opts ...PagerOption) Pager
 	Render() string
 	RenderCSV() string
 	RenderHTML() string
@@ -26,7 +27,6 @@ type Writer interface {
 	SetColumnConfigs(configs []ColumnConfig)
 	SetIndexColumn(colNum int)
 	SetOutputMirror(mirror io.Writer)
-	SetPageSize(numLines int)
 	SetRowPainter(painter RowPainter)
 	SetStyle(style Style)
 	SetTitle(format string, a ...interface{})
@@ -37,6 +37,8 @@ type Writer interface {
 
 	// deprecated; in favor of Style().HTML.CSSClass
 	SetHTMLCSSClass(cssClass string)
+	// deprecated; in favor of Pager()
+	SetPageSize(numLines int)
 }
 
 // NewWriter initializes and returns a Writer.


Test patch:
diff --git a/table/pager_test.go b/table/pager_test.go
new file mode 100644
index 0000000..ec431b6
--- /dev/null
+++ b/table/pager_test.go
@@ -0,0 +1,107 @@
+package table
+
+import (
+	"github.com/stretchr/testify/assert"
+	"strings"
+	"testing"
+)
+
+func TestPager(t *testing.T) {
+	expectedOutput := `+-------------+----------+--------+-----------------------------------------------------+--------+-----+-------+-------+------------------+---------+-------+----------+
+| PASSENGERID | SURVIVED | PCLASS | NAME                                                | SEX    | AGE | SIBSP | PARCH | TICKET           | FARE    | CABIN | EMBARKED |
++-------------+----------+--------+-----------------------------------------------------+--------+-----+-------+-------+------------------+---------+-------+----------+
+| 1           | 0        | 3      | Braund, Mr. Owen Harris                             | male   | 22  | 1     | 0     | A/5 21171        | 7.25    |       | S        |
+| 2           | 1        | 1      | Cumings, Mrs. John Bradley (Florence Briggs Thayer) | female | 38  | 1     | 0     | PC 17599         | 71.2833 | C85   | C        |
+| 3           | 1        | 3      | Heikkinen, Miss. Laina                              | female | 26  | 0     | 0     | STON/O2. 3101282 | 7.925   |       | S        |
+| 4           | 1        | 1      | Futrelle, Mrs. Jacques Heath (Lily May Peel)        | female | 35  | 1     | 0     | 113803           | 53.1    | C123  | S        |
+| 5           | 0        | 3      | Allen, Mr. William Henry                            | male   | 35  | 0     | 0     | 373450           | 8.05    |       | S        |
+| 6           | 0        | 3      | Moran, Mr. James                                    | male   |     | 0     | 0     | 330877           | 8.4583  |       | Q        |
+| 7           | 0        | 1      | McCarthy, Mr. Timothy J                             | male   | 54  | 0     | 0     | 17463            | 51.8625 | E46   | S        |
+| 8           | 0        | 3      | Palsson, Master. Gosta Leonard                      | male   | 2   | 3     | 1     | 349909           | 21.075  |       | S        |
+| 9           | 1        | 3      | Johnson, Mrs. Oscar W (Elisabeth Vilhelmina Berg)   | female | 27  | 0     | 2     | 347742           | 11.1333 |       | S        |
+| 10          | 1        | 2      | Nasser, Mrs. Nicholas (Adele Achem)                 | female | 14  | 1     | 0     | 237736           | 30.0708 |       | C        |
++-------------+----------+--------+-----------------------------------------------------+--------+-----+-------+-------+------------------+---------+-------+----------+`
+	expectedOutputP1 := `+-------------+----------+--------+-----------------------------------------------------+--------+-----+-------+-------+------------------+---------+-------+----------+
+| PASSENGERID | SURVIVED | PCLASS | NAME                                                | SEX    | AGE | SIBSP | PARCH | TICKET           | FARE    | CABIN | EMBARKED |
++-------------+----------+--------+-----------------------------------------------------+--------+-----+-------+-------+------------------+---------+-------+----------+
+| 1           | 0        | 3      | Braund, Mr. Owen Harris                             | male   | 22  | 1     | 0     | A/5 21171        | 7.25    |       | S        |
+| 2           | 1        | 1      | Cumings, Mrs. John Bradley (Florence Briggs Thayer) | female | 38  | 1     | 0     | PC 17599         | 71.2833 | C85   | C        |
+| 3           | 1        | 3      | Heikkinen, Miss. Laina                              | female | 26  | 0     | 0     | STON/O2. 3101282 | 7.925   |       | S        |
++-------------+----------+--------+-----------------------------------------------------+--------+-----+-------+-------+------------------+---------+-------+----------+`
+	expectedOutputP2 := `+-------------+----------+--------+-----------------------------------------------------+--------+-----+-------+-------+------------------+---------+-------+----------+
+| PASSENGERID | SURVIVED | PCLASS | NAME                                                | SEX    | AGE | SIBSP | PARCH | TICKET           | FARE    | CABIN | EMBARKED |
++-------------+----------+--------+-----------------------------------------------------+--------+-----+-------+-------+------------------+---------+-------+----------+
+| 4           | 1        | 1      | Futrelle, Mrs. Jacques Heath (Lily May Peel)        | female | 35  | 1     | 0     | 113803           | 53.1    | C123  | S        |
+| 5           | 0        | 3      | Allen, Mr. William Henry                            | male   | 35  | 0     | 0     | 373450           | 8.05    |       | S        |
+| 6           | 0        | 3      | Moran, Mr. James                                    | male   |     | 0     | 0     | 330877           | 8.4583  |       | Q        |
++-------------+----------+--------+-----------------------------------------------------+--------+-----+-------+-------+------------------+---------+-------+----------+`
+	expectedOutputP3 := `+-------------+----------+--------+-----------------------------------------------------+--------+-----+-------+-------+------------------+---------+-------+----------+
+| PASSENGERID | SURVIVED | PCLASS | NAME                                                | SEX    | AGE | SIBSP | PARCH | TICKET           | FARE    | CABIN | EMBARKED |
++-------------+----------+--------+-----------------------------------------------------+--------+-----+-------+-------+------------------+---------+-------+----------+
+| 7           | 0        | 1      | McCarthy, Mr. Timothy J                             | male   | 54  | 0     | 0     | 17463            | 51.8625 | E46   | S        |
+| 8           | 0        | 3      | Palsson, Master. Gosta Leonard                      | male   | 2   | 3     | 1     | 349909           | 21.075  |       | S        |
+| 9           | 1        | 3      | Johnson, Mrs. Oscar W (Elisabeth Vilhelmina Berg)   | female | 27  | 0     | 2     | 347742           | 11.1333 |       | S        |
++-------------+----------+--------+-----------------------------------------------------+--------+-----+-------+-------+------------------+---------+-------+----------+`
+	expectedOutputP4 := `+-------------+----------+--------+-----------------------------------------------------+--------+-----+-------+-------+------------------+---------+-------+----------+
+| PASSENGERID | SURVIVED | PCLASS | NAME                                                | SEX    | AGE | SIBSP | PARCH | TICKET           | FARE    | CABIN | EMBARKED |
++-------------+----------+--------+-----------------------------------------------------+--------+-----+-------+-------+------------------+---------+-------+----------+
+| 10          | 1        | 2      | Nasser, Mrs. Nicholas (Adele Achem)                 | female | 14  | 1     | 0     | 237736           | 30.0708 |       | C        |
++-------------+----------+--------+-----------------------------------------------------+--------+-----+-------+-------+------------------+---------+-------+----------+`
+
+	tw := NewWriter()
+	tw.AppendHeader(testTitanicHeader)
+	tw.AppendRows(testTitanicRows)
+	compareOutput(t, expectedOutput, tw.Render())
+
+	p := tw.Pager(PageSize(3))
+	assert.Equal(t, 1, p.Location())
+	compareOutput(t, expectedOutputP1, p.Render())
+	compareOutput(t, expectedOutputP2, p.Next())
+	compareOutput(t, expectedOutputP2, p.Render())
+	assert.Equal(t, 2, p.Location())
+	compareOutput(t, expectedOutputP3, p.Next())
+	compareOutput(t, expectedOutputP3, p.Render())
+	assert.Equal(t, 3, p.Location())
+	compareOutput(t, expectedOutputP4, p.Next())
+	compareOutput(t, expectedOutputP4, p.Render())
+	assert.Equal(t, 4, p.Location())
+	compareOutput(t, expectedOutputP4, p.Next())
+	compareOutput(t, expectedOutputP4, p.Render())
+	assert.Equal(t, 4, p.Location())
+	compareOutput(t, expectedOutputP3, p.Prev())
+	compareOutput(t, expectedOutputP3, p.Render())
+	assert.Equal(t, 3, p.Location())
+	compareOutput(t, expectedOutputP2, p.Prev())
+	compareOutput(t, expectedOutputP2, p.Render())
+	assert.Equal(t, 2, p.Location())
+	compareOutput(t, expectedOutputP1, p.Prev())
+	compareOutput(t, expectedOutputP1, p.Render())
+	assert.Equal(t, 1, p.Location())
+	compareOutput(t, expectedOutputP1, p.Prev())
+	compareOutput(t, expectedOutputP1, p.Render())
+	assert.Equal(t, 1, p.Location())
+
+	compareOutput(t, expectedOutputP1, p.GoTo(0))
+	compareOutput(t, expectedOutputP1, p.Render())
+	assert.Equal(t, 1, p.Location())
+	compareOutput(t, expectedOutputP1, p.GoTo(1))
+	compareOutput(t, expectedOutputP1, p.Render())
+	assert.Equal(t, 1, p.Location())
+	compareOutput(t, expectedOutputP2, p.GoTo(2))
+	compareOutput(t, expectedOutputP2, p.Render())
+	assert.Equal(t, 2, p.Location())
+	compareOutput(t, expectedOutputP3, p.GoTo(3))
+	compareOutput(t, expectedOutputP3, p.Render())
+	assert.Equal(t, 3, p.Location())
+	compareOutput(t, expectedOutputP4, p.GoTo(4))
+	compareOutput(t, expectedOutputP4, p.Render())
+	assert.Equal(t, 4, p.Location())
+	compareOutput(t, expectedOutputP4, p.GoTo(5))
+	compareOutput(t, expectedOutputP4, p.Render())
+	assert.Equal(t, 4, p.Location())
+
+	sb := strings.Builder{}
+	p.SetOutputMirror(&sb)
+	p.Render()
+	compareOutput(t, expectedOutputP4, sb.String())
+}
diff --git a/table/table_test.go b/table/table_test.go
index 9addf88..4eb7951 100644
--- a/table/table_test.go
+++ b/table/table_test.go
@@ -349,10 +349,10 @@ func TestTable_SetOutputMirror(t *testing.T) {
 
 func TestTable_SePageSize(t *testing.T) {
 	table := Table{}
-	assert.Equal(t, 0, table.pageSize)
+	assert.Equal(t, 0, table.pager.size)
 
 	table.SetPageSize(13)
-	assert.Equal(t, 13, table.pageSize)
+	assert.Equal(t, 13, table.pager.size)
 }
 
 func TestTable_SortByColumn(t *testing.T) {