From 2a8f60e9cc619e6289e69076f0f4f4cdd38ec63c Mon Sep 17 00:00:00 2001 From: Naveen Mahalingam Date: Wed, 2 Oct 2024 13:39:01 -0700 Subject: [PATCH] table: Pager to page through the output (#331) --- table/pager.go | 70 +++++++++++++++++++++++++++ table/pager_options.go | 11 +++++ table/pager_test.go | 107 +++++++++++++++++++++++++++++++++++++++++ table/render.go | 2 +- table/table.go | 48 +++++++++++++----- table/table_test.go | 4 +- table/writer.go | 4 +- 7 files changed, 231 insertions(+), 15 deletions(-) create mode 100644 table/pager.go create mode 100644 table/pager_options.go create mode 100644 table/pager_test.go 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/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/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/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) { 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.