Skip to content

Commit

Permalink
table: Pager to page through the output (#331)
Browse files Browse the repository at this point in the history
  • Loading branch information
jedib0t authored Oct 2, 2024
1 parent 6ea4b17 commit 2a8f60e
Show file tree
Hide file tree
Showing 7 changed files with 231 additions and 15 deletions.
70 changes: 70 additions & 0 deletions table/pager.go
Original file line number Diff line number Diff line change
@@ -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
}
11 changes: 11 additions & 0 deletions table/pager_options.go
Original file line number Diff line number Diff line change
@@ -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
}
}
107 changes: 107 additions & 0 deletions table/pager_test.go
Original file line number Diff line number Diff line change
@@ -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())
}
2 changes: 1 addition & 1 deletion table/render.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
48 changes: 37 additions & 11 deletions table/table.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"io"
"strings"
"time"
"unicode"

"github.com/jedib0t/go-pretty/v6/text"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -252,14 +277,15 @@ 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
// header rows again. This can be useful when dealing with tables containing a
// 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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions table/table_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
4 changes: 3 additions & 1 deletion table/writer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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{})
Expand All @@ -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.
Expand Down

0 comments on commit 2a8f60e

Please sign in to comment.