Skip to content

Commit 2a8f60e

Browse files
authored
table: Pager to page through the output (#331)
1 parent 6ea4b17 commit 2a8f60e

File tree

7 files changed

+231
-15
lines changed

7 files changed

+231
-15
lines changed

table/pager.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package table
2+
3+
import (
4+
"io"
5+
)
6+
7+
// Pager lets you interact with the table rendering in a paged manner.
8+
type Pager interface {
9+
// GoTo moves to the given 1-indexed page number.
10+
GoTo(pageNum int) string
11+
// Location returns the current page number in 1-indexed form.
12+
Location() int
13+
// Next moves to the next available page and returns the same.
14+
Next() string
15+
// Prev moves to the previous available page and returns the same.
16+
Prev() string
17+
// Render returns the current page.
18+
Render() string
19+
// SetOutputMirror sets up the writer to which Render() will write the
20+
// output other than returning.
21+
SetOutputMirror(mirror io.Writer)
22+
}
23+
24+
type pager struct {
25+
index int // 0-indexed
26+
pages []string
27+
outputMirror io.Writer
28+
size int
29+
}
30+
31+
func (p *pager) GoTo(pageNum int) string {
32+
if pageNum < 1 {
33+
pageNum = 1
34+
}
35+
if pageNum > len(p.pages) {
36+
pageNum = len(p.pages)
37+
}
38+
p.index = pageNum - 1
39+
return p.pages[p.index]
40+
}
41+
42+
func (p *pager) Location() int {
43+
return p.index + 1
44+
}
45+
46+
func (p *pager) Next() string {
47+
if p.index < len(p.pages)-1 {
48+
p.index++
49+
}
50+
return p.pages[p.index]
51+
}
52+
53+
func (p *pager) Prev() string {
54+
if p.index > 0 {
55+
p.index--
56+
}
57+
return p.pages[p.index]
58+
}
59+
60+
func (p *pager) Render() string {
61+
pageToWrite := p.pages[p.index]
62+
if p.outputMirror != nil {
63+
_, _ = p.outputMirror.Write([]byte(pageToWrite))
64+
}
65+
return pageToWrite
66+
}
67+
68+
func (p *pager) SetOutputMirror(mirror io.Writer) {
69+
p.outputMirror = mirror
70+
}

table/pager_options.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package table
2+
3+
// PagerOption helps control Paging.
4+
type PagerOption func(t *Table)
5+
6+
// PageSize sets the size of each page rendered.
7+
func PageSize(pageSize int) PagerOption {
8+
return func(t *Table) {
9+
t.pager.size = pageSize
10+
}
11+
}

table/pager_test.go

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package table
2+
3+
import (
4+
"github.com/stretchr/testify/assert"
5+
"strings"
6+
"testing"
7+
)
8+
9+
func TestPager(t *testing.T) {
10+
expectedOutput := `+-------------+----------+--------+-----------------------------------------------------+--------+-----+-------+-------+------------------+---------+-------+----------+
11+
| PASSENGERID | SURVIVED | PCLASS | NAME | SEX | AGE | SIBSP | PARCH | TICKET | FARE | CABIN | EMBARKED |
12+
+-------------+----------+--------+-----------------------------------------------------+--------+-----+-------+-------+------------------+---------+-------+----------+
13+
| 1 | 0 | 3 | Braund, Mr. Owen Harris | male | 22 | 1 | 0 | A/5 21171 | 7.25 | | S |
14+
| 2 | 1 | 1 | Cumings, Mrs. John Bradley (Florence Briggs Thayer) | female | 38 | 1 | 0 | PC 17599 | 71.2833 | C85 | C |
15+
| 3 | 1 | 3 | Heikkinen, Miss. Laina | female | 26 | 0 | 0 | STON/O2. 3101282 | 7.925 | | S |
16+
| 4 | 1 | 1 | Futrelle, Mrs. Jacques Heath (Lily May Peel) | female | 35 | 1 | 0 | 113803 | 53.1 | C123 | S |
17+
| 5 | 0 | 3 | Allen, Mr. William Henry | male | 35 | 0 | 0 | 373450 | 8.05 | | S |
18+
| 6 | 0 | 3 | Moran, Mr. James | male | | 0 | 0 | 330877 | 8.4583 | | Q |
19+
| 7 | 0 | 1 | McCarthy, Mr. Timothy J | male | 54 | 0 | 0 | 17463 | 51.8625 | E46 | S |
20+
| 8 | 0 | 3 | Palsson, Master. Gosta Leonard | male | 2 | 3 | 1 | 349909 | 21.075 | | S |
21+
| 9 | 1 | 3 | Johnson, Mrs. Oscar W (Elisabeth Vilhelmina Berg) | female | 27 | 0 | 2 | 347742 | 11.1333 | | S |
22+
| 10 | 1 | 2 | Nasser, Mrs. Nicholas (Adele Achem) | female | 14 | 1 | 0 | 237736 | 30.0708 | | C |
23+
+-------------+----------+--------+-----------------------------------------------------+--------+-----+-------+-------+------------------+---------+-------+----------+`
24+
expectedOutputP1 := `+-------------+----------+--------+-----------------------------------------------------+--------+-----+-------+-------+------------------+---------+-------+----------+
25+
| PASSENGERID | SURVIVED | PCLASS | NAME | SEX | AGE | SIBSP | PARCH | TICKET | FARE | CABIN | EMBARKED |
26+
+-------------+----------+--------+-----------------------------------------------------+--------+-----+-------+-------+------------------+---------+-------+----------+
27+
| 1 | 0 | 3 | Braund, Mr. Owen Harris | male | 22 | 1 | 0 | A/5 21171 | 7.25 | | S |
28+
| 2 | 1 | 1 | Cumings, Mrs. John Bradley (Florence Briggs Thayer) | female | 38 | 1 | 0 | PC 17599 | 71.2833 | C85 | C |
29+
| 3 | 1 | 3 | Heikkinen, Miss. Laina | female | 26 | 0 | 0 | STON/O2. 3101282 | 7.925 | | S |
30+
+-------------+----------+--------+-----------------------------------------------------+--------+-----+-------+-------+------------------+---------+-------+----------+`
31+
expectedOutputP2 := `+-------------+----------+--------+-----------------------------------------------------+--------+-----+-------+-------+------------------+---------+-------+----------+
32+
| PASSENGERID | SURVIVED | PCLASS | NAME | SEX | AGE | SIBSP | PARCH | TICKET | FARE | CABIN | EMBARKED |
33+
+-------------+----------+--------+-----------------------------------------------------+--------+-----+-------+-------+------------------+---------+-------+----------+
34+
| 4 | 1 | 1 | Futrelle, Mrs. Jacques Heath (Lily May Peel) | female | 35 | 1 | 0 | 113803 | 53.1 | C123 | S |
35+
| 5 | 0 | 3 | Allen, Mr. William Henry | male | 35 | 0 | 0 | 373450 | 8.05 | | S |
36+
| 6 | 0 | 3 | Moran, Mr. James | male | | 0 | 0 | 330877 | 8.4583 | | Q |
37+
+-------------+----------+--------+-----------------------------------------------------+--------+-----+-------+-------+------------------+---------+-------+----------+`
38+
expectedOutputP3 := `+-------------+----------+--------+-----------------------------------------------------+--------+-----+-------+-------+------------------+---------+-------+----------+
39+
| PASSENGERID | SURVIVED | PCLASS | NAME | SEX | AGE | SIBSP | PARCH | TICKET | FARE | CABIN | EMBARKED |
40+
+-------------+----------+--------+-----------------------------------------------------+--------+-----+-------+-------+------------------+---------+-------+----------+
41+
| 7 | 0 | 1 | McCarthy, Mr. Timothy J | male | 54 | 0 | 0 | 17463 | 51.8625 | E46 | S |
42+
| 8 | 0 | 3 | Palsson, Master. Gosta Leonard | male | 2 | 3 | 1 | 349909 | 21.075 | | S |
43+
| 9 | 1 | 3 | Johnson, Mrs. Oscar W (Elisabeth Vilhelmina Berg) | female | 27 | 0 | 2 | 347742 | 11.1333 | | S |
44+
+-------------+----------+--------+-----------------------------------------------------+--------+-----+-------+-------+------------------+---------+-------+----------+`
45+
expectedOutputP4 := `+-------------+----------+--------+-----------------------------------------------------+--------+-----+-------+-------+------------------+---------+-------+----------+
46+
| PASSENGERID | SURVIVED | PCLASS | NAME | SEX | AGE | SIBSP | PARCH | TICKET | FARE | CABIN | EMBARKED |
47+
+-------------+----------+--------+-----------------------------------------------------+--------+-----+-------+-------+------------------+---------+-------+----------+
48+
| 10 | 1 | 2 | Nasser, Mrs. Nicholas (Adele Achem) | female | 14 | 1 | 0 | 237736 | 30.0708 | | C |
49+
+-------------+----------+--------+-----------------------------------------------------+--------+-----+-------+-------+------------------+---------+-------+----------+`
50+
51+
tw := NewWriter()
52+
tw.AppendHeader(testTitanicHeader)
53+
tw.AppendRows(testTitanicRows)
54+
compareOutput(t, expectedOutput, tw.Render())
55+
56+
p := tw.Pager(PageSize(3))
57+
assert.Equal(t, 1, p.Location())
58+
compareOutput(t, expectedOutputP1, p.Render())
59+
compareOutput(t, expectedOutputP2, p.Next())
60+
compareOutput(t, expectedOutputP2, p.Render())
61+
assert.Equal(t, 2, p.Location())
62+
compareOutput(t, expectedOutputP3, p.Next())
63+
compareOutput(t, expectedOutputP3, p.Render())
64+
assert.Equal(t, 3, p.Location())
65+
compareOutput(t, expectedOutputP4, p.Next())
66+
compareOutput(t, expectedOutputP4, p.Render())
67+
assert.Equal(t, 4, p.Location())
68+
compareOutput(t, expectedOutputP4, p.Next())
69+
compareOutput(t, expectedOutputP4, p.Render())
70+
assert.Equal(t, 4, p.Location())
71+
compareOutput(t, expectedOutputP3, p.Prev())
72+
compareOutput(t, expectedOutputP3, p.Render())
73+
assert.Equal(t, 3, p.Location())
74+
compareOutput(t, expectedOutputP2, p.Prev())
75+
compareOutput(t, expectedOutputP2, p.Render())
76+
assert.Equal(t, 2, p.Location())
77+
compareOutput(t, expectedOutputP1, p.Prev())
78+
compareOutput(t, expectedOutputP1, p.Render())
79+
assert.Equal(t, 1, p.Location())
80+
compareOutput(t, expectedOutputP1, p.Prev())
81+
compareOutput(t, expectedOutputP1, p.Render())
82+
assert.Equal(t, 1, p.Location())
83+
84+
compareOutput(t, expectedOutputP1, p.GoTo(0))
85+
compareOutput(t, expectedOutputP1, p.Render())
86+
assert.Equal(t, 1, p.Location())
87+
compareOutput(t, expectedOutputP1, p.GoTo(1))
88+
compareOutput(t, expectedOutputP1, p.Render())
89+
assert.Equal(t, 1, p.Location())
90+
compareOutput(t, expectedOutputP2, p.GoTo(2))
91+
compareOutput(t, expectedOutputP2, p.Render())
92+
assert.Equal(t, 2, p.Location())
93+
compareOutput(t, expectedOutputP3, p.GoTo(3))
94+
compareOutput(t, expectedOutputP3, p.Render())
95+
assert.Equal(t, 3, p.Location())
96+
compareOutput(t, expectedOutputP4, p.GoTo(4))
97+
compareOutput(t, expectedOutputP4, p.Render())
98+
assert.Equal(t, 4, p.Location())
99+
compareOutput(t, expectedOutputP4, p.GoTo(5))
100+
compareOutput(t, expectedOutputP4, p.Render())
101+
assert.Equal(t, 4, p.Location())
102+
103+
sb := strings.Builder{}
104+
p.SetOutputMirror(&sb)
105+
p.Render()
106+
compareOutput(t, expectedOutputP4, sb.String())
107+
}

table/render.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@ func (t *Table) renderLine(out *strings.Builder, row rowStr, hint renderHint) {
214214
// the header all over again with a spacing line
215215
if hint.isRegularNonSeparatorRow() {
216216
t.numLinesRendered++
217-
if t.pageSize > 0 && t.numLinesRendered%t.pageSize == 0 && !hint.isLastLineOfLastRow() {
217+
if t.pager.size > 0 && t.numLinesRendered%t.pager.size == 0 && !hint.isLastLineOfLastRow() {
218218
t.renderRowsFooter(out)
219219
t.renderRowsBorderBottom(out)
220220
out.WriteString(t.style.Box.PageSeparator)

table/table.go

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"fmt"
55
"io"
66
"strings"
7+
"time"
78
"unicode"
89

910
"github.com/jedib0t/go-pretty/v6/text"
@@ -68,10 +69,8 @@ type Table struct {
6869
numLinesRendered int
6970
// outputMirror stores an io.Writer where the "Render" functions would write
7071
outputMirror io.Writer
71-
// pageSize stores the maximum lines to render before rendering the header
72-
// again (to denote a page break) - useful when you are dealing with really
73-
// long tables
74-
pageSize int
72+
// pager controls how the output is separated into pages
73+
pager pager
7574
// rows stores the rows that make up the body (in string form)
7675
rows []rowStr
7776
// rowsColors stores the text.Colors over-rides for each row as defined by
@@ -109,8 +108,8 @@ type Table struct {
109108
// suppressEmptyColumns hides columns which have no content on all regular
110109
// rows
111110
suppressEmptyColumns bool
112-
// supressTrailingSpaces removes all trailing spaces from the end of the last column
113-
supressTrailingSpaces bool
111+
// suppressTrailingSpaces removes all trailing spaces from the end of the last column
112+
suppressTrailingSpaces bool
114113
// title contains the text to appear above the table
115114
title string
116115
}
@@ -192,6 +191,32 @@ func (t *Table) Length() int {
192191
return len(t.rowsRaw)
193192
}
194193

194+
// Pager returns an object that splits the table output into pages and
195+
// lets you move back and forth through them.
196+
func (t *Table) Pager(opts ...PagerOption) Pager {
197+
for _, opt := range opts {
198+
opt(t)
199+
}
200+
201+
// use a temporary page separator for splitting up the pages
202+
tempPageSep := fmt.Sprintf("%p // page separator // %d", t.rows, time.Now().UnixNano())
203+
204+
// backup
205+
origOutputMirror, origPageSep := t.outputMirror, t.Style().Box.PageSeparator
206+
// restore on exit
207+
defer func() {
208+
t.outputMirror = origOutputMirror
209+
t.Style().Box.PageSeparator = origPageSep
210+
}()
211+
// override
212+
t.outputMirror = nil
213+
t.Style().Box.PageSeparator = tempPageSep
214+
// render
215+
t.pager.pages = strings.Split(t.Render(), tempPageSep)
216+
217+
return &t.pager
218+
}
219+
195220
// ResetFooters resets and clears all the Footer rows appended earlier.
196221
func (t *Table) ResetFooters() {
197222
t.rowsFooterRaw = nil
@@ -252,14 +277,15 @@ func (t *Table) SetIndexColumn(colNum int) {
252277
// in addition to returning a string.
253278
func (t *Table) SetOutputMirror(mirror io.Writer) {
254279
t.outputMirror = mirror
280+
t.pager.SetOutputMirror(mirror)
255281
}
256282

257283
// SetPageSize sets the maximum number of lines to render before rendering the
258284
// header rows again. This can be useful when dealing with tables containing a
259285
// long list of rows that can span pages. Please note that the pagination logic
260286
// will not consider Header/Footer lines for paging.
261287
func (t *Table) SetPageSize(numLines int) {
262-
t.pageSize = numLines
288+
t.pager.size = numLines
263289
}
264290

265291
// SetRowPainter sets the RowPainter function which determines the colors to use
@@ -304,7 +330,7 @@ func (t *Table) SuppressEmptyColumns() {
304330

305331
// SuppressTrailingSpaces removes all trailing spaces from the output.
306332
func (t *Table) SuppressTrailingSpaces() {
307-
t.supressTrailingSpaces = true
333+
t.suppressTrailingSpaces = true
308334
}
309335

310336
func (t *Table) getAlign(colIdx int, hint renderHint) text.Align {
@@ -689,7 +715,7 @@ func (t *Table) isIndexColumn(colIdx int, hint renderHint) bool {
689715

690716
func (t *Table) render(out *strings.Builder) string {
691717
outStr := out.String()
692-
if t.supressTrailingSpaces {
718+
if t.suppressTrailingSpaces {
693719
var trimmed []string
694720
for _, line := range strings.Split(outStr, "\n") {
695721
trimmed = append(trimmed, strings.TrimRightFunc(line, unicode.IsSpace))
@@ -786,8 +812,8 @@ func (t *Table) shouldSeparateRows(rowIdx int, numRows int) bool {
786812
}
787813

788814
pageSize := numRows
789-
if t.pageSize > 0 {
790-
pageSize = t.pageSize
815+
if t.pager.size > 0 {
816+
pageSize = t.pager.size
791817
}
792818
if rowIdx%pageSize == pageSize-1 { // last row of page
793819
return false

table/table_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -349,10 +349,10 @@ func TestTable_SetOutputMirror(t *testing.T) {
349349

350350
func TestTable_SePageSize(t *testing.T) {
351351
table := Table{}
352-
assert.Equal(t, 0, table.pageSize)
352+
assert.Equal(t, 0, table.pager.size)
353353

354354
table.SetPageSize(13)
355-
assert.Equal(t, 13, table.pageSize)
355+
assert.Equal(t, 13, table.pager.size)
356356
}
357357

358358
func TestTable_SortByColumn(t *testing.T) {

table/writer.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ type Writer interface {
1212
AppendRows(rows []Row, configs ...RowConfig)
1313
AppendSeparator()
1414
Length() int
15+
Pager(opts ...PagerOption) Pager
1516
Render() string
1617
RenderCSV() string
1718
RenderHTML() string
@@ -26,7 +27,6 @@ type Writer interface {
2627
SetColumnConfigs(configs []ColumnConfig)
2728
SetIndexColumn(colNum int)
2829
SetOutputMirror(mirror io.Writer)
29-
SetPageSize(numLines int)
3030
SetRowPainter(painter RowPainter)
3131
SetStyle(style Style)
3232
SetTitle(format string, a ...interface{})
@@ -37,6 +37,8 @@ type Writer interface {
3737

3838
// deprecated; in favor of Style().HTML.CSSClass
3939
SetHTMLCSSClass(cssClass string)
40+
// deprecated; in favor of Pager()
41+
SetPageSize(numLines int)
4042
}
4143

4244
// NewWriter initializes and returns a Writer.

0 commit comments

Comments
 (0)