Skip to content

Commit a875259

Browse files
author
Maciej Więcek
authored
Multiline cells (#159)
* chore: add hardcoded cells wrap with reflow.wordwrap * feat: add multiline support * fix: remove unncessery align * chore: restore a new line * chore: restore align * refactor the multilinelogic * feat: add example-multiline * feat: add multiline tests * chore: append one more test
1 parent d6ce119 commit a875259

File tree

9 files changed

+279
-4
lines changed

9 files changed

+279
-4
lines changed

Makefile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ example-events:
1818
example-features:
1919
@go run ./examples/features/main.go
2020

21+
.PHONY: example-multiline
22+
example-multiline:
23+
@go run ./examples/multiline/main.go
24+
2125
.PHONY: example-filter
2226
example-filter:
2327
@go run ./examples/filter/*.go

examples/multiline/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Multiline Example
2+
3+
This example code showcases the implementation of a multiline feature. The feature enables users to input and display content spanning multiple lines within the row. The provided code allows you to integrate the multiline feature seamlessly into your project. Feel free to experiment and adapt the code based on your specific requirements.
4+
5+
<img width="593" alt="image" src="https://github.com/Evertras/bubble-table/assets/23465248/3092b6f2-1e75-4c11-85f6-fcbea249d509">

examples/multiline/main.go

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
package main
2+
3+
import (
4+
"log"
5+
"strings"
6+
7+
tea "github.com/charmbracelet/bubbletea"
8+
"github.com/charmbracelet/lipgloss"
9+
"github.com/evertras/bubble-table/table"
10+
)
11+
12+
const (
13+
columnKeyName = "name"
14+
columnKeyCountry = "country"
15+
columnKeyCurrency = "crurrency"
16+
)
17+
18+
type Model struct {
19+
tableModel table.Model
20+
}
21+
22+
func NewModel() Model {
23+
columns := []table.Column{
24+
table.NewColumn(columnKeyName, "Name", 10).WithStyle(
25+
lipgloss.NewStyle().
26+
Foreground(lipgloss.Color("#88f")),
27+
),
28+
table.NewColumn(columnKeyCountry, "Country", 20),
29+
table.NewColumn(columnKeyCurrency, "Currency", 10),
30+
}
31+
32+
rows := []table.Row{
33+
table.NewRow(
34+
table.RowData{
35+
columnKeyName: "Talon Stokes",
36+
columnKeyCountry: "Mexico",
37+
columnKeyCurrency: "$23.17",
38+
}),
39+
table.NewRow(
40+
table.RowData{
41+
columnKeyName: "Sonia Shepard",
42+
columnKeyCountry: "United States",
43+
columnKeyCurrency: "$76.47",
44+
}),
45+
table.NewRow(
46+
table.RowData{
47+
columnKeyName: "Shad Reed",
48+
columnKeyCountry: "Turkey",
49+
columnKeyCurrency: "$62.99",
50+
}),
51+
table.NewRow(
52+
table.RowData{
53+
columnKeyName: "Kibo Clay",
54+
columnKeyCountry: "Philippines",
55+
columnKeyCurrency: "$29.82",
56+
}),
57+
table.NewRow(
58+
table.RowData{
59+
60+
columnKeyName: "Leslie Kerr",
61+
columnKeyCountry: "Singapore",
62+
columnKeyCurrency: "$70.54",
63+
}),
64+
table.NewRow(
65+
table.RowData{
66+
columnKeyName: "Micah Hurst",
67+
columnKeyCountry: "Pakistan",
68+
columnKeyCurrency: "$80.84",
69+
}),
70+
table.NewRow(
71+
table.RowData{
72+
columnKeyName: "Dora Miranda",
73+
columnKeyCountry: "Colombia",
74+
columnKeyCurrency: "$34.75",
75+
}),
76+
table.NewRow(
77+
table.RowData{
78+
columnKeyName: "Keefe Walters",
79+
columnKeyCountry: "China",
80+
columnKeyCurrency: "$56.82",
81+
}),
82+
table.NewRow(
83+
table.RowData{
84+
columnKeyName: "Fujimoto Tarokizaemon no shoutokinori",
85+
columnKeyCountry: "Japan",
86+
columnKeyCurrency: "$89.31",
87+
}),
88+
table.NewRow(
89+
table.RowData{
90+
columnKeyName: "Keefe Walters",
91+
columnKeyCountry: "China",
92+
columnKeyCurrency: "$56.82",
93+
}),
94+
table.NewRow(
95+
table.RowData{
96+
columnKeyName: "Vincent Sanchez",
97+
columnKeyCountry: "Peru",
98+
columnKeyCurrency: "$71.60",
99+
}),
100+
table.NewRow(
101+
table.RowData{
102+
columnKeyName: "Lani Figueroa",
103+
columnKeyCountry: "United Kingdom",
104+
columnKeyCurrency: "$90.67",
105+
}),
106+
}
107+
108+
model := Model{
109+
tableModel: table.New(columns).
110+
WithRows(rows).
111+
HeaderStyle(lipgloss.NewStyle().Foreground(lipgloss.Color("10")).Bold(true)).
112+
Focused(true).
113+
WithBaseStyle(
114+
lipgloss.NewStyle().
115+
BorderForeground(lipgloss.Color("#a38")).
116+
Foreground(lipgloss.Color("#a7a")).
117+
Align(lipgloss.Left),
118+
).
119+
WithMultiline(true),
120+
}
121+
122+
return model
123+
}
124+
125+
func (m Model) Init() tea.Cmd {
126+
return nil
127+
}
128+
129+
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
130+
var (
131+
cmd tea.Cmd
132+
cmds []tea.Cmd
133+
)
134+
135+
m.tableModel, cmd = m.tableModel.Update(msg)
136+
cmds = append(cmds, cmd)
137+
138+
switch msg := msg.(type) {
139+
case tea.KeyMsg:
140+
switch msg.String() {
141+
case "ctrl+c", "esc", "q":
142+
cmds = append(cmds, tea.Quit)
143+
}
144+
}
145+
146+
return m, tea.Batch(cmds...)
147+
}
148+
149+
func (m Model) View() string {
150+
body := strings.Builder{}
151+
152+
body.WriteString("A table demo with multiline feature enabled!\n")
153+
body.WriteString("Press up/down or j/k to move around\n")
154+
body.WriteString(m.tableModel.View())
155+
body.WriteString("\n")
156+
157+
return body.String()
158+
}
159+
160+
func main() {
161+
p := tea.NewProgram(NewModel())
162+
163+
if err := p.Start(); err != nil {
164+
log.Fatal(err)
165+
}
166+
}

table/border.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -354,7 +354,7 @@ func (b *borderStyleRow) inherit(s lipgloss.Style) {
354354
}
355355

356356
// There's a lot of branches here, but splitting it up further would make it
357-
// harder to follow. So just be careful with comments and make sure it's tested!
357+
// harder to follow. So just be careful with comments and make sure it's tested!
358358
//
359359
//nolint:nestif
360360
func (m Model) styleHeaders() borderStyleRow {

table/model.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,9 @@ type Model struct {
8989
// Internal cached calculation, the height of the header and footer
9090
// including borders. Used to determine how many padding rows to add.
9191
metaHeight int
92+
93+
// If true, the table will be multiline
94+
multiline bool
9295
}
9396

9497
// New creates a new table ready for further modifications.

table/options.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,3 +424,10 @@ func (m Model) WithAllRowsDeselected() Model {
424424

425425
return m
426426
}
427+
428+
// WithMultiline sets whether or not to wrap text in cells to multiple lines.
429+
func (m Model) WithMultiline(multiline bool) Model {
430+
m.multiline = multiline
431+
432+
return m
433+
}

table/row.go

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"fmt"
55

66
"github.com/charmbracelet/lipgloss"
7+
"github.com/muesli/reflow/wordwrap"
78
)
89

910
// RowData is a map of string column keys to interface{} data. Data with a key
@@ -43,7 +44,7 @@ func (r Row) WithStyle(style lipgloss.Style) Row {
4344
return r
4445
}
4546

46-
//nolint:nestif // This has many ifs, but they're short
47+
//nolint:nestif,cyclop // This has many ifs, but they're short
4748
func (m Model) renderRowColumnData(row Row, column Column, rowStyle lipgloss.Style, borderStyle lipgloss.Style) string {
4849
cellStyle := rowStyle.Copy().Inherit(column.style).Inherit(m.baseStyle)
4950

@@ -86,8 +87,15 @@ func (m Model) renderRowColumnData(row Row, column Column, rowStyle lipgloss.Sty
8687
}
8788
}
8889

90+
if m.multiline {
91+
str = wordwrap.String(str, column.width)
92+
cellStyle = cellStyle.Align(lipgloss.Top)
93+
} else {
94+
str = limitStr(str, column.width)
95+
}
96+
8997
cellStyle = cellStyle.Inherit(borderStyle)
90-
cellStr := cellStyle.Render(limitStr(str, column.width))
98+
cellStr := cellStyle.Render(str)
9199

92100
return cellStr
93101
}
@@ -121,6 +129,14 @@ func (m Model) renderRowData(row Row, rowStyle lipgloss.Style, last bool) string
121129

122130
stylesInner, stylesLast := m.styleRows()
123131

132+
maxCellHeight := 1
133+
if m.multiline {
134+
for _, column := range m.columns {
135+
cellStr := m.renderRowColumnData(row, column, rowStyle, lipgloss.NewStyle())
136+
maxCellHeight = max(maxCellHeight, lipgloss.Height(cellStr))
137+
}
138+
}
139+
124140
for columnIndex, column := range m.columns {
125141
var borderStyle lipgloss.Style
126142
var rowStyles borderStyleRow
@@ -130,6 +146,7 @@ func (m Model) renderRowData(row Row, rowStyle lipgloss.Style, last bool) string
130146
} else {
131147
rowStyles = stylesLast
132148
}
149+
rowStyle = rowStyle.Copy().Height(maxCellHeight)
133150

134151
if m.horizontalScrollOffsetCol > 0 && columnIndex == m.horizontalScrollFreezeColumnsCount {
135152
var borderStyle lipgloss.Style

table/view.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import (
66
"github.com/charmbracelet/lipgloss"
77
)
88

9-
// View renders the table. It does not end in a newline, so that it can be
9+
// View renders the table. It does not end in a newline, so that it can be
1010
// composed with other elements more consistently.
1111
//
1212
//nolint:cyclop

table/view_test.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1515,3 +1515,76 @@ func TestMinimumHeightSmallerThanTable(t *testing.T) {
15151515

15161516
assert.Equal(t, expectedTable, rendered)
15171517
}
1518+
1519+
func TestMultilineEnabled(t *testing.T) {
1520+
model := New([]Column{
1521+
NewColumn("name", "Name", 4),
1522+
}).
1523+
WithRows([]Row{
1524+
NewRow(RowData{"name": "AAAAAAAAAAAAAAAAAA"}),
1525+
NewRow(RowData{"name": "BBB"}),
1526+
}).
1527+
WithMultiline(true)
1528+
1529+
assert.True(t, model.multiline)
1530+
1531+
const expectedTable = `┏━━━━┓
1532+
┃Name┃
1533+
┣━━━━┫
1534+
┃AAAA┃
1535+
┃AAAA┃
1536+
┃AAAA┃
1537+
┃AAAA┃
1538+
┃AA ┃
1539+
┃BBB ┃
1540+
┗━━━━┛`
1541+
1542+
rendered := model.View()
1543+
assert.Equal(t, expectedTable, rendered)
1544+
}
1545+
1546+
func TestMultilineDisabledByDefault(t *testing.T) {
1547+
model := New([]Column{
1548+
NewColumn("name", "Name", 4),
1549+
}).
1550+
WithRows([]Row{
1551+
NewRow(RowData{"name": "AAAAAAAAAAAAAAAAAA"}),
1552+
NewRow(RowData{"name": "BBB"}),
1553+
})
1554+
// WithMultiline(false)
1555+
1556+
assert.False(t, model.multiline)
1557+
1558+
const expectedTable = `┏━━━━┓
1559+
┃Name┃
1560+
┣━━━━┫
1561+
┃AAA…┃
1562+
┃ BBB┃
1563+
┗━━━━┛`
1564+
1565+
rendered := model.View()
1566+
assert.Equal(t, expectedTable, rendered)
1567+
}
1568+
1569+
func TestMultilineDisabledExplicite(t *testing.T) {
1570+
model := New([]Column{
1571+
NewColumn("name", "Name", 4),
1572+
}).
1573+
WithRows([]Row{
1574+
NewRow(RowData{"name": "AAAAAAAAAAAAAAAAAA"}),
1575+
NewRow(RowData{"name": "BBB"}),
1576+
}).
1577+
WithMultiline(false)
1578+
1579+
assert.False(t, model.multiline)
1580+
1581+
const expectedTable = `┏━━━━┓
1582+
┃Name┃
1583+
┣━━━━┫
1584+
┃AAA…┃
1585+
┃ BBB┃
1586+
┗━━━━┛`
1587+
1588+
rendered := model.View()
1589+
assert.Equal(t, expectedTable, rendered)
1590+
}

0 commit comments

Comments
 (0)