diff --git a/examples/table/chess/main.go b/examples/table/chess/main.go index b80cd920..f4e9a8d8 100644 --- a/examples/table/chess/main.go +++ b/examples/table/chess/main.go @@ -13,7 +13,7 @@ func main() { re := lipgloss.NewRenderer(os.Stdout) labelStyle := re.NewStyle().Foreground(lipgloss.Color("241")) - board := [][]any{ + board := [][]string{ {"♜", "♞", "♝", "♛", "♚", "♝", "♞", "♜"}, {"♟", "♟", "♟", "♟", "♟", "♟", "♟", "♟"}, {" ", " ", " ", " ", " ", " ", " ", " "}, @@ -28,7 +28,7 @@ func main() { Border(lipgloss.NormalBorder()). BorderRow(true). BorderColumn(true). - Rows(board...). + Rows(table.Rows(board...)). StyleFunc(func(row, col int) lipgloss.Style { return lipgloss.NewStyle().Padding(0, 1) }) diff --git a/examples/table/languages/main.go b/examples/table/languages/main.go index 418da63c..4e7513d3 100644 --- a/examples/table/languages/main.go +++ b/examples/table/languages/main.go @@ -30,7 +30,7 @@ func main() { BorderStyle = lipgloss.NewStyle().Foreground(purple) ) - rows := [][]any{ + rows := [][]string{ {"Chinese", "您好", "你好"}, {"Japanese", "こんにちは", "やあ"}, {"Arabic", "أهلين", "أهلا"}, @@ -67,7 +67,7 @@ func main() { return style }). Headers("LANGUAGE", "FORMAL", "INFORMAL"). - Rows(rows...). + Rows(table.Rows(rows...)). Width(18) fmt.Println(t) diff --git a/examples/table/mindy/main.go b/examples/table/mindy/main.go index f345333e..dbdfd4d0 100644 --- a/examples/table/mindy/main.go +++ b/examples/table/mindy/main.go @@ -13,7 +13,7 @@ func main() { labelStyle := re.NewStyle().Width(3).Align(lipgloss.Right) swatchStyle := re.NewStyle().Width(6) - data := [][]any{} + data := [][]string{} for i := 0; i < 13; i += 8 { data = append(data, makeRow(i, i+5)) } @@ -32,7 +32,7 @@ func main() { t := table.New(). Border(lipgloss.HiddenBorder()). - Rows(data...). + Rows(table.Rows(data...)). StyleFunc(func(row, col int) lipgloss.Style { color := lipgloss.Color(fmt.Sprint(data[row-1][col-col%2])) switch { @@ -48,10 +48,10 @@ func main() { const rowLength = 12 -func makeRow(start, end int) []any { - var row []any +func makeRow(start, end int) []string { + var row []string for i := start; i <= end; i++ { - row = append(row, i) + row = append(row, fmt.Sprint(i)) row = append(row, "") } for i := len(row); i < rowLength; i++ { @@ -60,6 +60,6 @@ func makeRow(start, end int) []any { return row } -func makeEmptyRow() []any { +func makeEmptyRow() []string { return makeRow(0, -1) } diff --git a/examples/table/pokemon/main.go b/examples/table/pokemon/main.go index ea7dbc79..4a11bbdf 100644 --- a/examples/table/pokemon/main.go +++ b/examples/table/pokemon/main.go @@ -27,35 +27,35 @@ func main() { } headers := []any{"#", "Name", "Type 1", "Type 2", "Japanese", "Official Rom."} - data := [][]any{ - {1, "Bulbasaur", "Grass", "Poison", "フシギダネ", "Bulbasaur"}, - {2, "Ivysaur", "Grass", "Poison", "フシギソウ", "Ivysaur"}, - {3, "Venusaur", "Grass", "Poison", "フシギバナ", "Venusaur"}, - {4, "Charmander", "Fire", "", "ヒトカゲ", "Hitokage"}, - {5, "Charmeleon", "Fire", "", "リザード", "Lizardo"}, - {6, "Charizard", "Fire", "Flying", "リザードン", "Lizardon"}, - {7, "Squirtle", "Water", "", "ゼニガメ", "Zenigame"}, - {8, "Wartortle", "Water", "", "カメール", "Kameil"}, - {9, "Blastoise", "Water", "", "カメックス", "Kamex"}, - {10, "Caterpie", "Bug", "", "キャタピー", "Caterpie"}, - {11, "Metapod", "Bug", "", "トランセル", "Trancell"}, - {12, "Butterfree", "Bug", "Flying", "バタフリー", "Butterfree"}, - {13, "Weedle", "Bug", "Poison", "ビードル", "Beedle"}, - {14, "Kakuna", "Bug", "Poison", "コクーン", "Cocoon"}, - {15, "Beedrill", "Bug", "Poison", "スピアー", "Spear"}, - {16, "Pidgey", "Normal", "Flying", "ポッポ", "Poppo"}, - {17, "Pidgeotto", "Normal", "Flying", "ピジョン", "Pigeon"}, - {18, "Pidgeot", "Normal", "Flying", "ピジョット", "Pigeot"}, - {19, "Rattata", "Normal", "", "コラッタ", "Koratta"}, - {20, "Raticate", "Normal", "", "ラッタ", "Ratta"}, - {21, "Spearow", "Normal", "Flying", "オニスズメ", "Onisuzume"}, - {22, "Fearow", "Normal", "Flying", "オニドリル", "Onidrill"}, - {23, "Ekans", "Poison", "", "アーボ", "Arbo"}, - {24, "Arbok", "Poison", "", "アーボック", "Arbok"}, - {25, "Pikachu", "Electric", "", "ピカチュウ", "Pikachu"}, - {26, "Raichu", "Electric", "", "ライチュウ", "Raichu"}, - {27, "Sandshrew", "Ground", "", "サンド", "Sand"}, - {28, "Sandslash", "Ground", "", "サンドパン", "Sandpan"}, + data := [][]string{ + {"1", "Bulbasaur", "Grass", "Poison", "フシギダネ", "Bulbasaur"}, + {"2", "Ivysaur", "Grass", "Poison", "フシギソウ", "Ivysaur"}, + {"3", "Venusaur", "Grass", "Poison", "フシギバナ", "Venusaur"}, + {"4", "Charmander", "Fire", "", "ヒトカゲ", "Hitokage"}, + {"5", "Charmeleon", "Fire", "", "リザード", "Lizardo"}, + {"6", "Charizard", "Fire", "Flying", "リザードン", "Lizardon"}, + {"7", "Squirtle", "Water", "", "ゼニガメ", "Zenigame"}, + {"8", "Wartortle", "Water", "", "カメール", "Kameil"}, + {"9", "Blastoise", "Water", "", "カメックス", "Kamex"}, + {"10", "Caterpie", "Bug", "", "キャタピー", "Caterpie"}, + {"11", "Metapod", "Bug", "", "トランセル", "Trancell"}, + {"12", "Butterfree", "Bug", "Flying", "バタフリー", "Butterfree"}, + {"13", "Weedle", "Bug", "Poison", "ビードル", "Beedle"}, + {"14", "Kakuna", "Bug", "Poison", "コクーン", "Cocoon"}, + {"15", "Beedrill", "Bug", "Poison", "スピアー", "Spear"}, + {"16", "Pidgey", "Normal", "Flying", "ポッポ", "Poppo"}, + {"17", "Pidgeotto", "Normal", "Flying", "ピジョン", "Pigeon"}, + {"18", "Pidgeot", "Normal", "Flying", "ピジョット", "Pigeot"}, + {"19", "Rattata", "Normal", "", "コラッタ", "Koratta"}, + {"20", "Raticate", "Normal", "", "ラッタ", "Ratta"}, + {"21", "Spearow", "Normal", "Flying", "オニスズメ", "Onisuzume"}, + {"22", "Fearow", "Normal", "Flying", "オニドリル", "Onidrill"}, + {"23", "Ekans", "Poison", "", "アーボ", "Arbo"}, + {"24", "Arbok", "Poison", "", "アーボック", "Arbok"}, + {"25", "Pikachu", "Electric", "", "ピカチュウ", "Pikachu"}, + {"26", "Raichu", "Electric", "", "ライチュウ", "Raichu"}, + {"27", "Sandshrew", "Ground", "", "サンド", "Sand"}, + {"28", "Sandslash", "Ground", "", "サンドパン", "Sandpan"}, } CapitalizeHeaders := func(data []any) []any { @@ -69,8 +69,8 @@ func main() { Border(lipgloss.NormalBorder()). BorderStyle(re.NewStyle().Foreground(lipgloss.Color("238"))). Headers(CapitalizeHeaders(headers)...). - Width(40). - Rows(data...). + Width(80). + Rows(table.Rows(data...)). StyleFunc(func(row, col int) lipgloss.Style { if row == 0 { return headerStyle diff --git a/table/rows.go b/table/rows.go new file mode 100644 index 00000000..d5a94c06 --- /dev/null +++ b/table/rows.go @@ -0,0 +1,133 @@ +package table + +// Data is the interface that wraps the basic methods of a table model. +type Data interface { + Row(row int) Row + Append(row Row) + Count() int + Columns() int +} + +// Row represents one line in the table. +type Row interface { + Column(col int) string + Length() int +} + +// StringData is a string-based implementation of the Data interface. +type StringData struct { + rows []Row + columns int +} + +// Rows creates a new StringData with the given number of columns. +func Rows(rows ...[]string) *StringData { + m := StringData{columns: 0} + + for _, row := range rows { + m.columns = max(m.columns, len(row)) + m.rows = append(m.rows, StringRow(row)) + } + + return &m +} + +// Append appends the given row to the table. +func (m *StringData) Append(row Row) { + m.columns = max(m.columns, row.Length()) + m.rows = append(m.rows, row) +} + +// Row returns the row at the given index. +func (m *StringData) Row(row int) Row { + return m.rows[row] +} + +// Columns returns the number of columns in the table. +func (m *StringData) Columns() int { + return m.columns +} + +// Item appends the given row to the table. +func (m *StringData) Item(rows ...string) *StringData { + m.columns = max(m.columns, len(rows)) + m.rows = append(m.rows, StringRow(rows)) + return m +} + +// Count returns the number of rows in the table. +func (m *StringData) 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] +} + +// Value returns the value of the column at the given index. +func (r StringRow) Length() int { + return len(r) +} + +// Filter applies a filter on some data. +type Filter struct { + data Data + filter func(row Row) bool +} + +// NewFilter initializes a new Filter. +func NewFilter(data Data) *Filter { + return &Filter{data: data} +} + +// Filter applies the given filter function to the data. +func (m *Filter) Filter(f func(row Row) bool) *Filter { + m.filter = f + return m +} + +// Row returns the row at the given index. +func (m *Filter) Row(row int) Row { + j := 0 + for i := 0; i < m.data.Count(); i++ { + if m.filter(m.data.Row(i)) { + if j == row { + return m.data.Row(i) + } + + j++ + } + } + + return nil +} + +// Append appends the given row to the table. +func (m *Filter) Append(row Row) { + m.data.Append(row) +} + +// Columns returns the number of columns in the table. +func (m *Filter) Columns() int { + return m.data.Columns() +} + +// Count returns the number of rows in the table. +func (m *Filter) Count() int { + j := 0 + for i := 0; i < m.data.Count(); i++ { + if m.filter(m.data.Row(i)) { + j++ + } + } + + return j +} diff --git a/table/table.go b/table/table.go index 4dd2582a..b8c612e5 100644 --- a/table/table.go +++ b/table/table.go @@ -53,9 +53,11 @@ type Table struct { borderStyle lipgloss.Style headers []any - rows [][]any + data Data - width int + width int + height int + offset int // widths tracks the width of each column. widths []int @@ -83,7 +85,7 @@ func New() *Table { // ClearRows clears the table rows. func (t *Table) ClearRows() *Table { - t.rows = make([][]any, 0) + t.data = nil return t } @@ -101,15 +103,18 @@ 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 +// Rows sets the table data. +func (t *Table) Rows(data Data) *Table { + t.data = data return t } -// Row appends a row of data to the table. -func (t *Table) Row(row ...any) *Table { - t.rows = append(t.rows, row) +// Row appends a row to the table data. +func (t *Table) Row(data ...string) *Table { + if t.data == nil { + t.data = Rows() + } + t.data.Append(StringRow(data)) return t } @@ -181,10 +186,22 @@ 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.data != nil && t.data.Count() > 0 if !hasHeaders && !hasRows { return "" @@ -192,23 +209,17 @@ func (t *Table) String() string { 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.data.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.data.Columns()) + t.heights = make([]int, btoi(hasHeaders)+t.data.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 @@ -218,9 +229,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.data.Count(); r++ { + row := t.data.Row(r) + + for i := 0; i < t.data.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)) } @@ -278,12 +293,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.data.Count()) + for r := 0; r < t.data.Count(); r++ { + row := t.data.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) } @@ -328,7 +346,8 @@ func (t *Table) String() string { s.WriteString("\n") } - for r, row := range t.rows { + for r := t.offset; r < t.data.Count(); r++ { + row := t.data.Row(r) s.WriteString(t.constructRow(r, row)) } @@ -355,7 +374,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.data.Count()*btoi(t.borderRow) } // Render returns the table as a string. @@ -492,33 +511,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.data.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.data.Columns()-1 && t.borderColumn { cells = append(cells, left) } } @@ -534,7 +549,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.data.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]))) diff --git a/table/table_test.go b/table/table_test.go index d583fa40..ca186d59 100644 --- a/table/table_test.go +++ b/table/table_test.go @@ -46,16 +46,50 @@ func TestTable(t *testing.T) { } } -func TestTableBorder(t *testing.T) { +func TestTableOffset(t *testing.T) { table := New(). - Border(lipgloss.DoubleBorder()). + Border(lipgloss.NormalBorder()). StyleFunc(TableStyle). Headers("LANGUAGE", "FORMAL", "INFORMAL"). Row("Chinese", "Nǐn hǎo", "Nǐ hǎo"). Row("French", "Bonjour", "Salut"). Row("Japanese", "こんにちは", "やあ"). Row("Russian", "Zdravstvuyte", "Privet"). - Row("Spanish", "Hola", "¿Qué tal?") + Row("Spanish", "Hola", "¿Qué tal?"). + Offset(1) + + expected := strings.TrimSpace(` +┌──────────┬──────────────┬───────────┐ +│ LANGUAGE │ FORMAL │ INFORMAL │ +├──────────┼──────────────┼───────────┤ +│ French │ Bonjour │ Salut │ +│ Japanese │ こんにちは │ やあ │ +│ Russian │ Zdravstvuyte │ Privet │ +│ Spanish │ Hola │ ¿Qué tal? │ +└──────────┴──────────────┴───────────┘ +`) + + if table.String() != expected { + t.Fatalf("expected:\n\n%s\n\ngot:\n\n%s", expected, table.String()) + } +} + +func TestTableBorder(t *testing.T) { + data := [][]string{ + {"Chinese", "Nǐn hǎo", "Nǐ hǎo"}, + {"French", "Bonjour", "Salut"}, + {"Japanese", "こんにちは", "やあ"}, + {"Russian", "Zdravstvuyte", "Privet"}, + {"Spanish", "Hola", "¿Qué tal?"}, + } + + rows := Rows(data...) + + table := New(). + Border(lipgloss.DoubleBorder()). + StyleFunc(TableStyle). + Headers("LANGUAGE", "FORMAL", "INFORMAL"). + Rows(rows) expected := strings.TrimSpace(` ╔══════════╦══════════════╦═══════════╗ @@ -75,19 +109,20 @@ func TestTableBorder(t *testing.T) { } func TestTableSetRows(t *testing.T) { - rows := [][]any{ + data := [][]string{ {"Chinese", "Nǐn hǎo", "Nǐ hǎo"}, {"French", "Bonjour", "Salut"}, {"Japanese", "こんにちは", "やあ"}, {"Russian", "Zdravstvuyte", "Privet"}, {"Spanish", "Hola", "¿Qué tal?"}, } + rows := Rows(data...) table := New(). Border(lipgloss.NormalBorder()). StyleFunc(TableStyle). Headers("LANGUAGE", "FORMAL", "INFORMAL"). - Rows(rows...) + Rows(rows) expected := strings.TrimSpace(` ┌──────────┬──────────────┬───────────┐ @@ -107,19 +142,20 @@ func TestTableSetRows(t *testing.T) { } func TestMoreCellsThanHeaders(t *testing.T) { - rows := [][]any{ + data := [][]string{ {"Chinese", "Nǐn hǎo", "Nǐ hǎo"}, {"French", "Bonjour", "Salut"}, {"Japanese", "こんにちは", "やあ"}, {"Russian", "Zdravstvuyte", "Privet"}, {"Spanish", "Hola", "¿Qué tal?"}, } + rows := Rows(data...) table := New(). Border(lipgloss.NormalBorder()). StyleFunc(TableStyle). Headers("LANGUAGE", "FORMAL"). - Rows(rows...) + Rows(rows) expected := strings.TrimSpace(` ┌──────────┬──────────────┬───────────┐ @@ -139,19 +175,20 @@ func TestMoreCellsThanHeaders(t *testing.T) { } func TestMoreCellsThanHeadersExtra(t *testing.T) { - rows := [][]any{ + data := [][]string{ {"Chinese", "Nǐn hǎo", "Nǐ hǎo"}, {"French", "Bonjour", "Salut", "Salut"}, {"Japanese", "こんにちは", "やあ"}, {"Russian", "Zdravstvuyte", "Privet", "Privet", "Privet"}, {"Spanish", "Hola", "¿Qué tal?"}, } + rows := Rows(data...) table := New(). Border(lipgloss.NormalBorder()). StyleFunc(TableStyle). Headers("LANGUAGE", "FORMAL"). - Rows(rows...) + Rows(rows) expected := strings.TrimSpace(` ┌──────────┬──────────────┬───────────┬────────┬────────┐ @@ -251,20 +288,21 @@ func TestTableNoColumnSeparatorsWithHeaders(t *testing.T) { } func TestBorderColumnsWithExtraRows(t *testing.T) { - rows := [][]any{ + data := [][]string{ {"Chinese", "Nǐn hǎo", "Nǐ hǎo"}, {"French", "Bonjour", "Salut", "Salut"}, {"Japanese", "こんにちは", "やあ"}, {"Russian", "Zdravstvuyte", "Privet", "Privet", "Privet"}, {"Spanish", "Hola", "¿Qué tal?"}, } + rows := Rows(data...) table := New(). Border(lipgloss.NormalBorder()). BorderColumn(false). StyleFunc(TableStyle). Headers("LANGUAGE", "FORMAL"). - Rows(rows...) + Rows(rows) expected := strings.TrimSpace(` ┌───────────────────────────────────────────────────┐ @@ -292,15 +330,19 @@ func TestNew(t *testing.T) { } func TestTableUnsetBorders(t *testing.T) { + data := [][]string{ + {"Chinese", "Nǐn hǎo", "Nǐ hǎo"}, + {"French", "Bonjour", "Salut"}, + {"Japanese", "こんにちは", "やあ"}, + {"Russian", "Zdravstvuyte", "Privet"}, + {"Spanish", "Hola", "¿Qué tal?"}, + } + table := New(). Border(lipgloss.NormalBorder()). StyleFunc(TableStyle). Headers("LANGUAGE", "FORMAL", "INFORMAL"). - Row("Chinese", "Nǐn hǎo", "Nǐ hǎo"). - Row("French", "Bonjour", "Salut"). - Row("Japanese", "こんにちは", "やあ"). - Row("Russian", "Zdravstvuyte", "Privet"). - Row("Spanish", "Hola", "¿Qué tal?"). + Rows(Rows(data...)). BorderTop(false). BorderBottom(false). BorderLeft(false). @@ -321,15 +363,19 @@ func TestTableUnsetBorders(t *testing.T) { } func TestTableUnsetHeaderSeparator(t *testing.T) { + data := [][]string{ + {"Chinese", "Nǐn hǎo", "Nǐ hǎo"}, + {"French", "Bonjour", "Salut"}, + {"Japanese", "こんにちは", "やあ"}, + {"Russian", "Zdravstvuyte", "Privet"}, + {"Spanish", "Hola", "¿Qué tal?"}, + } + table := New(). Border(lipgloss.NormalBorder()). StyleFunc(TableStyle). Headers("LANGUAGE", "FORMAL", "INFORMAL"). - Row("Chinese", "Nǐn hǎo", "Nǐ hǎo"). - Row("French", "Bonjour", "Salut"). - Row("Japanese", "こんにちは", "やあ"). - Row("Russian", "Zdravstvuyte", "Privet"). - Row("Spanish", "Hola", "¿Qué tal?"). + Rows(Rows(data...)). BorderHeader(false). BorderTop(false). BorderBottom(false). @@ -350,15 +396,19 @@ func TestTableUnsetHeaderSeparator(t *testing.T) { } func TestTableUnsetHeaderSeparatorWithBorder(t *testing.T) { + data := [][]string{ + {"Chinese", "Nǐn hǎo", "Nǐ hǎo"}, + {"French", "Bonjour", "Salut"}, + {"Japanese", "こんにちは", "やあ"}, + {"Russian", "Zdravstvuyte", "Privet"}, + {"Spanish", "Hola", "¿Qué tal?"}, + } + table := New(). Border(lipgloss.NormalBorder()). StyleFunc(TableStyle). Headers("LANGUAGE", "FORMAL", "INFORMAL"). - Row("Chinese", "Nǐn hǎo", "Nǐ hǎo"). - Row("French", "Bonjour", "Salut"). - Row("Japanese", "こんにちは", "やあ"). - Row("Russian", "Zdravstvuyte", "Privet"). - Row("Spanish", "Hola", "¿Qué tal?"). + Rows(Rows(data...)). BorderHeader(false) expected := strings.TrimSpace(` @@ -378,16 +428,20 @@ func TestTableUnsetHeaderSeparatorWithBorder(t *testing.T) { } func TestTableRowSeparators(t *testing.T) { + data := [][]string{ + {"Chinese", "Nǐn hǎo", "Nǐ hǎo"}, + {"French", "Bonjour", "Salut"}, + {"Japanese", "こんにちは", "やあ"}, + {"Russian", "Zdravstvuyte", "Privet"}, + {"Spanish", "Hola", "¿Qué tal?"}, + } + table := New(). Border(lipgloss.NormalBorder()). StyleFunc(TableStyle). BorderRow(true). Headers("LANGUAGE", "FORMAL", "INFORMAL"). - Row("Chinese", "Nǐn hǎo", "Nǐ hǎo"). - Row("French", "Bonjour", "Salut"). - Row("Japanese", "こんにちは", "やあ"). - Row("Russian", "Zdravstvuyte", "Privet"). - Row("Spanish", "Hola", "¿Qué tal?") + Rows(Rows(data...)) expected := strings.TrimSpace(` ┌──────────┬──────────────┬───────────┐ @@ -413,7 +467,7 @@ func TestTableRowSeparators(t *testing.T) { func TestTableHeights(t *testing.T) { styleFunc := func(row, col int) lipgloss.Style { if row == 0 { - return lipgloss.NewStyle().Bold(true).Padding(0, 1) + return lipgloss.NewStyle().Padding(0, 1) } if col == 0 { return lipgloss.NewStyle().Width(18).Padding(1) @@ -421,13 +475,17 @@ func TestTableHeights(t *testing.T) { return lipgloss.NewStyle().Width(25).Padding(1, 2) } + data := [][]string{ + {"Chutar o balde", `Literally translates to "kick the bucket." It's used when someone gives up or loses patience.`}, + {"Engolir sapos", `Literally means "to swallow frogs." It's used to describe someone who has to tolerate or endure unpleasant situations.`}, + {"Arroz de festa", `Literally means "party rice." It´s used to refer to someone who shows up everywhere.`}, + } + table := New(). Border(lipgloss.NormalBorder()). StyleFunc(styleFunc). Headers("EXPRESSION", "MEANING"). - Row("Chutar o balde", `Literally translates to "kick the bucket." It's used when someone gives up or loses patience.`). - Row("Engolir sapos", `Literally means "to swallow frogs." It's used to describe someone who has to tolerate or endure unpleasant situations.`). - Row("Arroz de festa", `Literally means "party rice." It´s used to refer to someone who shows up everywhere.`) + Rows(Rows(data...)) expected := strings.TrimSpace(` ┌──────────────────┬─────────────────────────┐ @@ -467,7 +525,7 @@ func TestTableHeights(t *testing.T) { func TestTableMultiLineRowSeparator(t *testing.T) { styleFunc := func(row, col int) lipgloss.Style { if row == 0 { - return lipgloss.NewStyle().Bold(true).Padding(0, 1) + return lipgloss.NewStyle().Padding(0, 1) } if col == 0 { return lipgloss.NewStyle().Width(18).Padding(1) @@ -522,20 +580,21 @@ func TestTableMultiLineRowSeparator(t *testing.T) { } func TestTableWidthExpand(t *testing.T) { - rows := [][]any{ + data := [][]string{ {"Chinese", "Nǐn hǎo", "Nǐ hǎo"}, {"French", "Bonjour", "Salut"}, {"Japanese", "こんにちは", "やあ"}, {"Russian", "Zdravstvuyte", "Privet"}, {"Spanish", "Hola", "¿Qué tal?"}, } + rows := Rows(data...) table := New(). Width(80). StyleFunc(TableStyle). Border(lipgloss.NormalBorder()). Headers("LANGUAGE", "FORMAL", "INFORMAL"). - Rows(rows...) + Rows(rows) expected := strings.TrimSpace(` ┌────────────────────────┬────────────────────────────┬────────────────────────┐ @@ -559,20 +618,21 @@ func TestTableWidthExpand(t *testing.T) { } func TestTableWidthShrink(t *testing.T) { - rows := [][]any{ + data := [][]string{ {"Chinese", "Nǐn hǎo", "Nǐ hǎo"}, {"French", "Bonjour", "Salut"}, {"Japanese", "こんにちは", "やあ"}, {"Russian", "Zdravstvuyte", "Privet"}, {"Spanish", "Hola", "¿Qué tal?"}, } + rows := Rows(data...) table := New(). Width(30). StyleFunc(TableStyle). Border(lipgloss.NormalBorder()). Headers("LANGUAGE", "FORMAL", "INFORMAL"). - Rows(rows...) + Rows(rows) expected := strings.TrimSpace(` ┌─────────┬─────────┬────────┐ @@ -592,18 +652,19 @@ func TestTableWidthShrink(t *testing.T) { } func TestTableWidthSmartCrop(t *testing.T) { - rows := [][]any{ - {"Kini", 40, "New York"}, - {"Eli", 30, "London"}, - {"Iris", 20, "Paris"}, + data := [][]string{ + {"Kini", "40", "New York"}, + {"Eli", "30", "London"}, + {"Iris", "20", "Paris"}, } + rows := Rows(data...) table := New(). Width(25). StyleFunc(TableStyle). Border(lipgloss.NormalBorder()). Headers("Name", "Age of Person", "Location"). - Rows(rows...) + Rows(rows) expected := strings.TrimSpace(` ┌──────┬─────┬──────────┐ @@ -621,7 +682,7 @@ func TestTableWidthSmartCrop(t *testing.T) { } func TestTableWidthSmartCropExtensive(t *testing.T) { - rows := [][]any{ + data := [][]string{ {"Chinese", "您好", "你好"}, {"Japanese", "こんにちは", "やあ"}, {"Arabic", "أهلين", "أهلا"}, @@ -629,13 +690,14 @@ func TestTableWidthSmartCropExtensive(t *testing.T) { {"Spanish", "Hola", "¿Qué tal?"}, {"English", "You look absolutely fabulous.", "How's it going?"}, } + rows := Rows(data...) table := New(). Width(18). StyleFunc(TableStyle). Border(lipgloss.ThickBorder()). Headers("LANGUAGE", "FORMAL", "INFORMAL"). - Rows(rows...) + Rows(rows) expected := strings.TrimSpace(` ┏━━━━┳━━━━━┳━━━━━┓ @@ -656,7 +718,7 @@ func TestTableWidthSmartCropExtensive(t *testing.T) { } func TestTableWidthSmartCropTiny(t *testing.T) { - rows := [][]any{ + data := [][]string{ {"Chinese", "您好", "你好"}, {"Japanese", "こんにちは", "やあ"}, {"Russian", "Здравствуйте", "Привет"}, @@ -669,7 +731,7 @@ func TestTableWidthSmartCropTiny(t *testing.T) { StyleFunc(TableStyle). Border(lipgloss.NormalBorder()). Headers("LANGUAGE", "FORMAL", "INFORMAL"). - Rows(rows...) + Rows(Rows(data...)) expected := strings.TrimSpace(` ┌ @@ -689,7 +751,7 @@ func TestTableWidthSmartCropTiny(t *testing.T) { } func TestTableWidths(t *testing.T) { - rows := [][]any{ + rows := [][]string{ {"Chinese", "Nǐn hǎo", "Nǐ hǎo"}, {"French", "Bonjour", "Salut"}, {"Japanese", "こんにちは", "やあ"}, @@ -705,7 +767,7 @@ func TestTableWidths(t *testing.T) { Border(lipgloss.NormalBorder()). BorderColumn(false). Headers("LANGUAGE", "FORMAL", "INFORMAL"). - Rows(rows...) + Rows(Rows(rows...)) expected := strings.TrimSpace(` ────────────────────────────── @@ -725,7 +787,7 @@ func TestTableWidths(t *testing.T) { } func TestTableWidthShrinkNoBorders(t *testing.T) { - rows := [][]any{ + rows := [][]string{ {"Chinese", "Nǐn hǎo", "Nǐ hǎo"}, {"French", "Bonjour", "Salut"}, {"Japanese", "こんにちは", "やあ"}, @@ -741,7 +803,7 @@ func TestTableWidthShrinkNoBorders(t *testing.T) { Border(lipgloss.NormalBorder()). BorderColumn(false). Headers("LANGUAGE", "FORMAL", "INFORMAL"). - Rows(rows...) + Rows(Rows(rows...)) expected := strings.TrimSpace(` ────────────────────────────── @@ -760,6 +822,40 @@ func TestTableWidthShrinkNoBorders(t *testing.T) { } } +func TestFilter(t *testing.T) { + rows := Rows(). + Item("Chinese", "Nǐn hǎo", "Nǐ hǎo"). + Item("French", "Bonjour", "Salut"). + Item("Japanese", "こんにちは", "やあ"). + Item("Russian", "Zdravstvuyte", "Privet"). + Item("Spanish", "Hola", "¿Qué tal?") + + filter := NewFilter(rows).Filter(func(row Row) bool { + return row.Column(0) != "French" + }) + + table := New(). + Border(lipgloss.NormalBorder()). + StyleFunc(TableStyle). + Headers("LANGUAGE", "FORMAL", "INFORMAL"). + Rows(filter) + + expected := strings.TrimSpace(` +┌──────────┬──────────────┬───────────┐ +│ LANGUAGE │ FORMAL │ INFORMAL │ +├──────────┼──────────────┼───────────┤ +│ Chinese │ Nǐn hǎo │ Nǐ hǎo │ +│ Japanese │ こんにちは │ やあ │ +│ Russian │ Zdravstvuyte │ Privet │ +│ Spanish │ Hola │ ¿Qué tal? │ +└──────────┴──────────────┴───────────┘ +`) + + if table.String() != expected { + t.Fatalf("expected:\n\n%s\n\ngot:\n\n%s", expected, table.String()) + } +} + func debug(s string) string { return strings.ReplaceAll(s, " ", ".") }