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

Programmatic Sorting #27

Merged
merged 4 commits into from
Feb 23, 2022
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
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ example-pagination:
example-simplest:
@go run ./examples/simplest/*.go

.PHONY: example-simplest
example-sorting:
@go run ./examples/sorting/*.go

.PHONY: test
test:
@go test -race -cover ./table
Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,15 @@ Can make rows selectable, and fetch the current selections.
Pagination can be set with a given page size, which automatically generates a
simple footer to show the current page and total pages.

Columns can be sorted in either ascending or descending order. Multiple columns
can be specified in a row. If multiple columns are specified, first the table
is sorted by the first specified column, then each group within that column is
sorted in smaller and smaller groups. [See the sorting example](examples/sorting/main.go)
for more information.

If a feature is confusing to use or could use a better example, please feel free
to open an issue.

## Defining table data

A table is defined by a list of `Column` values that define the columns in the
Expand Down
13 changes: 9 additions & 4 deletions examples/features/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,11 @@ type Model struct {

func NewModel() Model {
columns := []table.Column{
table.NewColumn(columnKeyID, "ID", 5).WithStyle(lipgloss.NewStyle().Faint(true).Foreground(lipgloss.Color("#88f"))),
table.NewColumn(columnKeyID, "ID", 5).WithStyle(
lipgloss.NewStyle().
Faint(true).
Foreground(lipgloss.Color("#88f")).
Align(lipgloss.Left)),
table.NewColumn(columnKeyName, "Name", 10),
table.NewColumn(columnKeyDescription, "Description", 30),
table.NewColumn(columnKeyCount, "#", 5),
Expand All @@ -74,13 +78,13 @@ func NewModel() Model {
columnKeyCount: table.NewStyledCell(0, lipgloss.NewStyle().Faint(true)),
}),
table.NewRow(table.RowData{
columnKeyID: "2pg",
columnKeyID: "spg",
columnKeyName: "Page 2",
columnKeyDescription: "Second page",
columnKeyCount: 2,
}),
table.NewRow(table.RowData{
columnKeyID: "2pg2",
columnKeyID: "spg2",
columnKeyName: "Page 2.1",
columnKeyDescription: "Second page again",
columnKeyCount: 4,
Expand All @@ -103,7 +107,8 @@ func NewModel() Model {
WithKeyMap(keys).
WithStaticFooter("Footer!").
WithPageSize(3).
WithSelectedText(" ", "✓"),
WithSelectedText(" ", "✓").
SortByAsc(columnKeyID),
}

model.updateFooter()
Expand Down
108 changes: 108 additions & 0 deletions examples/sorting/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package main

import (
"log"
"strings"

tea "github.com/charmbracelet/bubbletea"
"github.com/evertras/bubble-table/table"
)

const (
columnKeyName = "name"
columnKeyType = "type"
columnKeyWins = "element"
)

type Model struct {
simpleTable table.Model

columnSortKey string
sortDirection string
}

func NewModel() Model {
return Model{
simpleTable: table.New([]table.Column{
table.NewColumn(columnKeyName, "Name", 13),
table.NewColumn(columnKeyType, "Type", 13),
table.NewColumn(columnKeyWins, "Wins", 5),
}).WithRows([]table.Row{
table.NewRow(table.RowData{
columnKeyName: "ピカピカ",
columnKeyType: "Pikachu",
columnKeyWins: 4,
}),
table.NewRow(table.RowData{
columnKeyName: "Alphonse",
columnKeyType: "Pikachu",
columnKeyWins: 13,
}),
table.NewRow(table.RowData{
columnKeyName: "Burninator",
columnKeyType: "Charmander",
columnKeyWins: 8,
}),
table.NewRow(table.RowData{
columnKeyName: "Dihydrogen Monoxide",
columnKeyType: "Squirtle",
columnKeyWins: 31,
}),
}),
}
}

func (m Model) Init() tea.Cmd {
return nil
}

func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var (
cmd tea.Cmd
cmds []tea.Cmd
)

m.simpleTable, cmd = m.simpleTable.Update(msg)
cmds = append(cmds, cmd)

switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "ctrl+c", "esc", "q":
cmds = append(cmds, tea.Quit)

case "n":
m.columnSortKey = columnKeyName
m.simpleTable = m.simpleTable.SortByAsc(m.columnSortKey)

case "t":
m.columnSortKey = columnKeyType
// Within the same type, order each by wins
m.simpleTable = m.simpleTable.SortByAsc(m.columnSortKey).ThenSortByDesc(columnKeyWins)

case "w":
m.columnSortKey = columnKeyWins
m.simpleTable = m.simpleTable.SortByDesc(m.columnSortKey)
}
}

return m, tea.Batch(cmds...)
}

func (m Model) View() string {
body := strings.Builder{}

body.WriteString("A sorted simple default table\nSort by (n)ame, (t)ype, or (w)ins\nCurrently sorting by: " + m.columnSortKey + "\nPress q or ctrl+c to quit\n\n")

body.WriteString(m.simpleTable.View())

return body.String()
}

func main() {
p := tea.NewProgram(NewModel())

if err := p.Start(); err != nil {
log.Fatal(err)
}
}
64 changes: 64 additions & 0 deletions table/data.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package table

import "time"

// This is just a bunch of data type checks, so... no linting here
// nolint: cyclop
func asInt(data interface{}) (int64, bool) {
switch val := data.(type) {
case int:
return int64(val), true

case int8:
return int64(val), true

case int16:
return int64(val), true

case int32:
return int64(val), true

case int64:
return val, true

case uint:
return int64(val), true

case uint8:
return int64(val), true

case uint16:
return int64(val), true

case uint32:
return int64(val), true

case uint64:
return int64(val), true

case time.Duration:
return int64(val), true

case StyledCell:
return asInt(val.Data)
}

return 0, false
}

func asNumber(data interface{}) (float64, bool) {
switch val := data.(type) {
case float32:
return float64(val), true

case float64:
return val, true

case StyledCell:
return asNumber(val.Data)
}

intVal, isInt := asInt(data)

return float64(intVal), isInt
}
43 changes: 43 additions & 0 deletions table/data_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package table

import (
"testing"
"time"

"github.com/stretchr/testify/assert"
)

func TestAsInt(t *testing.T) {
check := func(data interface{}, isInt bool, expectedValue int64) {
val, ok := asInt(data)
assert.Equal(t, isInt, ok)
assert.Equal(t, expectedValue, val)
}

check(3, true, 3)
check(3.3, false, 0)
check(int8(3), true, 3)
check(int16(3), true, 3)
check(int32(3), true, 3)
check(int64(3), true, 3)
check(uint(3), true, 3)
check(uint8(3), true, 3)
check(uint16(3), true, 3)
check(uint32(3), true, 3)
check(uint64(3), true, 3)
check(StyledCell{Data: 3}, true, 3)
check(time.Duration(3), true, 3)
}

func TestAsNumber(t *testing.T) {
check := func(data interface{}, isFloat bool, expectedValue float64) {
val, ok := asNumber(data)
assert.Equal(t, isFloat, ok)
assert.InDelta(t, expectedValue, val, 0.001)
}

check(uint32(3), true, 3)
check(3.3, true, 3.3)
check(float32(3.3), true, 3.3)
check(StyledCell{Data: 3.3}, true, 3.3)
}
4 changes: 4 additions & 0 deletions table/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ type Model struct {
pageSize int
currentPage int

// Sorting
sortOrder []sortColumn
sortedRows []Row

// Internal cached calculations for reference
totalWidth int
}
Expand Down
1 change: 1 addition & 0 deletions table/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ func (m Model) HeaderStyle(style lipgloss.Style) Model {
// WithRows sets the rows to show as data in the table.
func (m Model) WithRows(rows []Row) Model {
m.rows = rows
m.updateSortedRows()

return m
}
Expand Down
2 changes: 1 addition & 1 deletion table/row.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ func (r Row) WithStyle(style lipgloss.Style) Row {
// nolint: cyclop
func (m Model) renderRow(rowIndex int, last bool) string {
numColumns := len(m.columns)
row := m.rows[rowIndex]
row := m.sortedRows[rowIndex]
highlighted := rowIndex == m.rowCursorIndex

columnStrings := []string{}
Expand Down
Loading