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: Pager to page through the output #331

Merged
merged 2 commits into from
Oct 2, 2024
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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