Skip to content

Commit

Permalink
wip: provide model interface for table data
Browse files Browse the repository at this point in the history
  • Loading branch information
muesli committed Oct 4, 2023
1 parent 2687d82 commit 85bf6e5
Show file tree
Hide file tree
Showing 3 changed files with 345 additions and 119 deletions.
111 changes: 111 additions & 0 deletions table/model.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package table

// Model is the interface that wraps the basic methods of a table model.
type Model interface {
Row(row int) Row
Count() int
Columns() int
}

// Row represents one line in the table.
type Row interface {
Column(col int) string
}

// StringModel is a string-based implementation of the Model interface.
type StringModel struct {
rows []Row
columns int
}

// NewStringModel creates a new StringModel with the given number of columns.
func NewStringModel(columns int) *StringModel {
return &StringModel{columns: columns}
}

// Row returns the row at the given index.
func (m *StringModel) Row(row int) Row {
return m.rows[row]
}

// Columns returns the number of columns in the table.
func (m *StringModel) Columns() int {
return m.columns
}

// AppendRows appends the given rows to the table.
func (m *StringModel) AppendRows(rows ...[]string) *StringModel {
for _, row := range rows {
m.rows = append(m.rows, StringRow(row))
}

return m
}

// AppendRow appends the given row to the table.
func (m *StringModel) AppendRow(rows ...string) *StringModel {
m.rows = append(m.rows, StringRow(rows))

return m
}

// Count returns the number of rows in the table.
func (m *StringModel) Count() int {
return len(m.rows)
}

// StringRow is a simple implementation of the Row interface.
type StringRow []string

// Value returns the value of the column at the given index.
func (r StringRow) Column(col int) string {
if col >= len(r) {
return ""
}

return r[col]
}

type FilterModel struct {

Check warning on line 69 in table/model.go

View workflow job for this annotation

GitHub Actions / lint

exported: exported type FilterModel should have comment or be unexported (revive)
model Model
filter func(row Row) bool
}

func NewFilterModel(model Model) *FilterModel {

Check warning on line 74 in table/model.go

View workflow job for this annotation

GitHub Actions / lint

exported: exported function NewFilterModel should have comment or be unexported (revive)
return &FilterModel{model: model}
}

func (m *FilterModel) Filter(f func(row Row) bool) *FilterModel {

Check warning on line 78 in table/model.go

View workflow job for this annotation

GitHub Actions / lint

exported: exported method FilterModel.Filter should have comment or be unexported (revive)
m.filter = f
return m
}

func (m *FilterModel) Row(row int) Row {

Check warning on line 83 in table/model.go

View workflow job for this annotation

GitHub Actions / lint

exported: exported method FilterModel.Row should have comment or be unexported (revive)
j := 0
for i := 0; i < m.model.Count(); i++ {
if m.filter(m.model.Row(i)) {
if j == row {
return m.model.Row(i)
}

j++
}
}

return nil
}

func (m *FilterModel) Columns() int {

Check warning on line 98 in table/model.go

View workflow job for this annotation

GitHub Actions / lint

exported: exported method FilterModel.Columns should have comment or be unexported (revive)
return m.model.Columns()
}

func (m *FilterModel) Count() int {

Check warning on line 102 in table/model.go

View workflow job for this annotation

GitHub Actions / lint

exported: exported method FilterModel.Count should have comment or be unexported (revive)
j := 0
for i := 0; i < m.model.Count(); i++ {
if m.filter(m.model.Row(i)) {
j++
}
}

return j
}
88 changes: 47 additions & 41 deletions table/table.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,11 @@ type Table struct {

borderStyle lipgloss.Style
headers []any
rows [][]any
model Model

width int
width int
height int
offset int

// widths tracks the width of each column.
widths []int
Expand Down Expand Up @@ -83,7 +85,7 @@ func New() *Table {

// ClearRows clears the table rows.
func (t *Table) ClearRows() *Table {
t.rows = make([][]any, 0)
t.model = nil
return t
}

Expand All @@ -101,15 +103,9 @@ func (t *Table) style(row, col int) lipgloss.Style {
return t.styleFunc(row, col)
}

// Rows sets the table rows.
func (t *Table) Rows(rows ...[]any) *Table {
t.rows = rows
return t
}

// Row appends a row of data to the table.
func (t *Table) Row(row ...any) *Table {
t.rows = append(t.rows, row)
// Model sets the table model.
func (t *Table) Model(model Model) *Table {
t.model = model
return t
}

Expand Down Expand Up @@ -181,34 +177,40 @@ func (t *Table) Width(w int) *Table {
return t
}

// Height sets the table height.
func (t *Table) Height(h int) *Table {
t.height = h
return t
}

// Offset sets the table rendering offset.
func (t *Table) Offset(o int) *Table {
t.offset = o
return t
}

// String returns the table as a string.
func (t *Table) String() string {
hasHeaders := t.headers != nil && len(t.headers) > 0
hasRows := t.rows != nil && len(t.rows) > 0
hasRows := t.model != nil && t.model.Count() > 0

if !hasHeaders && !hasRows {
return ""
}

var s strings.Builder

// Find the longest row length.
longestRowLen := len(t.headers)
for _, row := range t.rows {
longestRowLen = max(longestRowLen, len(row))
}

// Add empty cells to the headers, until it's the same length as the longest
// row (only if there are at headers in the first place).
if hasHeaders {
for i := len(t.headers); i < longestRowLen; i++ {
for i := len(t.headers); i < t.model.Columns(); i++ {
t.headers = append(t.headers, "")
}
}

// Initialize the widths.
t.widths = make([]int, longestRowLen)
t.heights = make([]int, btoi(hasHeaders)+len(t.rows))
t.widths = make([]int, t.model.Columns())
t.heights = make([]int, btoi(hasHeaders)+t.model.Count())

// The style function may affect width of the table. It's possible to set
// the StyleFunc after the headers and rows. Update the widths for a final
Expand All @@ -218,9 +220,13 @@ func (t *Table) String() string {
t.heights[0] = max(t.heights[0], lipgloss.Height(t.style(0, i).Render(fmt.Sprint(cell))))
}

for r, row := range t.rows {
for i, cell := range row {
rendered := t.style(r+1, i).Render(fmt.Sprint(cell))
for r := 0; r < t.model.Count(); r++ {
row := t.model.Row(r)

for i := 0; i < t.model.Columns(); i++ {
cell := row.Column(i)

rendered := t.style(r+1, i).Render(cell)
t.heights[r+btoi(hasHeaders)] = max(t.heights[r+btoi(hasHeaders)], lipgloss.Height(rendered))
t.widths[i] = max(t.widths[i], lipgloss.Width(rendered))
}
Expand Down Expand Up @@ -278,12 +284,15 @@ func (t *Table) String() string {
// column, and shrink the columns based on the largest difference.
columnMedians := make([]int, len(t.widths))
for c := range t.widths {
trimmedWidth := make([]int, len(t.rows))
for r, row := range t.rows {
renderedCell := t.style(r+btoi(hasHeaders), c).Render(fmt.Sprint(row[c]))
trimmedWidth := make([]int, t.model.Count())
for r := 0; r < t.model.Count(); r++ {
row := t.model.Row(r)

renderedCell := t.style(r+btoi(hasHeaders), c).Render(row.Column(c))
nonWhitespaceChars := lipgloss.Width(strings.TrimRight(renderedCell, " "))
trimmedWidth[r] = nonWhitespaceChars + 1
}

columnMedians[c] = median(trimmedWidth)
}

Expand Down Expand Up @@ -328,7 +337,8 @@ func (t *Table) String() string {
s.WriteString("\n")
}

for r, row := range t.rows {
for r := t.offset; r < t.model.Count(); r++ {
row := t.model.Row(r)
s.WriteString(t.constructRow(r, row))
}

Expand All @@ -355,7 +365,7 @@ func (t *Table) computeHeight() int {
hasHeaders := t.headers != nil && len(t.headers) > 0
return sum(t.heights) - 1 + btoi(hasHeaders) +
btoi(t.borderTop) + btoi(t.borderBottom) +
btoi(t.borderHeader) + len(t.rows)*btoi(t.borderRow)
btoi(t.borderHeader) + t.model.Count()*btoi(t.borderRow)
}

// Render returns the table as a string.
Expand Down Expand Up @@ -492,33 +502,29 @@ func (t *Table) constructHeaders() string {
return s.String()
}

func (t *Table) constructRow(index int, row []any) string {
func (t *Table) constructRow(index int, row Row) string {
var s strings.Builder

hasHeaders := t.headers != nil && len(t.headers) > 0
height := t.heights[index+btoi(hasHeaders)]

// Append empty cells to the row, until it's the same length as the
// longest row.
for i := len(row); i < len(t.widths); i++ {
row = append(row, "")
}

var cells []string
left := strings.Repeat(t.borderStyle.Render(t.border.Left)+"\n", height)
if t.borderLeft {
cells = append(cells, left)
}

for c, cell := range row {
for c := 0; c < t.model.Columns(); c++ {
cell := row.Column(c)

cells = append(cells, t.style(index+1, c).
Height(height).
MaxHeight(height).
Width(t.widths[c]).
MaxWidth(t.widths[c]).
Render(runewidth.Truncate(fmt.Sprint(cell), t.widths[c]*height, "…")))
Render(runewidth.Truncate(cell, t.widths[c]*height, "…")))

if c < len(row)-1 && t.borderColumn {
if c < t.model.Columns()-1 && t.borderColumn {
cells = append(cells, left)
}
}
Expand All @@ -534,7 +540,7 @@ func (t *Table) constructRow(index int, row []any) string {

s.WriteString(lipgloss.JoinHorizontal(lipgloss.Top, cells...) + "\n")

if t.borderRow && index < len(t.rows)-1 {
if t.borderRow && index < t.model.Count()-1 {
s.WriteString(t.borderStyle.Render(t.border.MiddleLeft))
for i := 0; i < len(t.widths); i++ {
s.WriteString(t.borderStyle.Render(strings.Repeat(t.border.Bottom, t.widths[i])))
Expand Down
Loading

0 comments on commit 85bf6e5

Please sign in to comment.