Skip to content

Commit

Permalink
Static footer (#18)
Browse files Browse the repository at this point in the history
Adds a static footer option
  • Loading branch information
Evertras committed Feb 20, 2022
1 parent 253a720 commit cc947b2
Show file tree
Hide file tree
Showing 10 changed files with 212 additions and 11 deletions.
4 changes: 2 additions & 2 deletions README.md
Expand Up @@ -8,15 +8,15 @@

A table component for the [Bubble Tea framework](https://github.com/charmbracelet/bubbletea).

![Table](sample.png)
![Table with all features](https://user-images.githubusercontent.com/5923958/154826826-226346a7-96f9-43ca-a484-49e81b3ab45d.png)

[View sample source code](./examples/features/main.go)

## Features

For a code reference, please see the [full feature example](./examples/features/main.go).

Displays a table with a header, rows, and borders.
Displays a table with a header, rows, footer, and borders.

Border shape is customizable with a basic thick square default.

Expand Down
20 changes: 15 additions & 5 deletions examples/features/main.go
Expand Up @@ -79,21 +79,31 @@ func NewModel() Model {
keys.RowDown.SetKeys("j", "down", "s")
keys.RowUp.SetKeys("k", "up", "w")

return Model{
model := Model{
tableModel: table.New(columns).
WithRows(rows).
HeaderStyle(lipgloss.NewStyle().Foreground(lipgloss.Color("10")).Bold(true)).
SelectableRows(true).
Focused(true).
Border(customBorder).
WithKeyMap(keys),
WithKeyMap(keys).
WithStaticFooter("Footer!"),
}

model.updateFooter()

return model
}

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

func (m *Model) updateFooter() {
highlightedRow := m.tableModel.HighlightedRow()
m.tableModel = m.tableModel.WithStaticFooter(fmt.Sprintf("Currently looking at ID: %s", highlightedRow.Data[columnKeyID]))
}

func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var (
cmd tea.Cmd
Expand All @@ -103,6 +113,9 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.tableModel, cmd = m.tableModel.Update(msg)
cmds = append(cmds, cmd)

// We control the footer text, so make sure to update it
m.updateFooter()

switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
Expand All @@ -117,11 +130,8 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
func (m Model) View() string {
body := strings.Builder{}

highlightedRow := m.tableModel.HighlightedRow()
body.WriteString("Table demo with all features enabled!\nPress space/enter to select a row, q or ctrl+c to quit\n")

body.WriteString(fmt.Sprintf("Currently looking at ID: %s\n", highlightedRow.Data[columnKeyID]))

selectedIDs := []string{}

for _, row := range m.tableModel.SelectedRows() {
Expand Down
Binary file removed sample.png
Binary file not shown.
61 changes: 57 additions & 4 deletions table/border.go
Expand Up @@ -45,6 +45,9 @@ type Border struct {

// Style for a table with only one cell
styleSingleCell lipgloss.Style

// Style for the footer
styleFooter lipgloss.Style
}

var (
Expand Down Expand Up @@ -80,6 +83,34 @@ func (b *Border) generateStyles() {
b.generateSingleColumnStyles()
b.generateSingleRowStyles()
b.generateSingleCellStyle()

// For now a footer is just the same as a single column's last row
b.styleFooter = b.styleSingleColumnBottom.Copy().Align(lipgloss.Right)
}

func (b *Border) styleLeftWithFooter(original lipgloss.Style) lipgloss.Style {
border := original.GetBorderStyle()

border.BottomLeft = b.LeftJunction

return original.Copy().BorderStyle(border)
}

func (b *Border) styleRightWithFooter(original lipgloss.Style) lipgloss.Style {
border := original.GetBorderStyle()

border.BottomRight = b.RightJunction

return original.Copy().BorderStyle(border)
}

func (b *Border) styleBothWithFooter(original lipgloss.Style) lipgloss.Style {
border := original.GetBorderStyle()

border.BottomLeft = b.LeftJunction
border.BottomRight = b.RightJunction

return original.Copy().BorderStyle(border)
}

// This function is long, but it's just repetitive...
Expand Down Expand Up @@ -287,6 +318,9 @@ func (b *borderStyleRow) inherit(s lipgloss.Style) {
b.right = b.right.Copy().Inherit(s)
}

// There's a lot of branches here, but splitting it up further would make it
// harder to follow. So just be careful with comments and make sure it's tested!
// nolint:nestif
func (m Model) styleHeaders() borderStyleRow {
hasRows := len(m.rows) > 0
singleColumn := len(m.columns) == 1
Expand All @@ -302,19 +336,28 @@ func (m Model) styleHeaders() borderStyleRow {
if hasRows {
// Single column
styles.left = m.border.styleSingleColumnTop
styles.inner = m.border.styleSingleColumnTop
styles.right = m.border.styleSingleColumnTop
styles.inner = styles.left
styles.right = styles.left
} else {
// Single cell
styles.left = m.border.styleSingleCell
styles.inner = m.border.styleSingleCell
styles.right = m.border.styleSingleCell
styles.inner = styles.left
styles.right = styles.left

if m.hasFooter() {
styles.left = m.border.styleBothWithFooter(styles.left)
}
}
} else if !hasRows {
// Single row
styles.left = m.border.styleSingleRowLeft
styles.inner = m.border.styleSingleRowInner
styles.right = m.border.styleSingleRowRight

if m.hasFooter() {
styles.left = m.border.styleLeftWithFooter(styles.left)
styles.right = m.border.styleRightWithFooter(styles.right)
}
} else {
// Multi
styles.left = m.border.styleMultiTopLeft
Expand All @@ -334,6 +377,11 @@ func (m Model) styleRows() (inner borderStyleRow, last borderStyleRow) {
inner.right = inner.left

last.left = m.border.styleSingleColumnBottom

if m.hasFooter() {
last.left = m.border.styleBothWithFooter(last.left)
}

last.inner = last.left
last.right = last.left
} else {
Expand All @@ -344,6 +392,11 @@ func (m Model) styleRows() (inner borderStyleRow, last borderStyleRow) {
last.left = m.border.styleMultiBottomLeft
last.inner = m.border.styleMultiBottom
last.right = m.border.styleMultiBottomRight

if m.hasFooter() {
last.left = m.border.styleLeftWithFooter(last.left)
last.right = m.border.styleRightWithFooter(last.right)
}
}

return inner, last
Expand Down
11 changes: 11 additions & 0 deletions table/dimensions.go
@@ -0,0 +1,11 @@
package table

func (m *Model) recalculateWidth() {
total := 0

for _, column := range m.columns {
total += column.Width
}

m.totalWidth = total + len(m.columns) - 1
}
13 changes: 13 additions & 0 deletions table/footer.go
@@ -0,0 +1,13 @@
package table

func (m Model) hasFooter() bool {
return m.staticFooter != ""
}

func (m Model) renderFooter() string {
if !m.hasFooter() {
return ""
}

return m.border.styleFooter.Width(m.totalWidth).Render(m.staticFooter)
}
6 changes: 6 additions & 0 deletions table/model.go
Expand Up @@ -19,6 +19,8 @@ type Model struct {
columns []Column
headerStyle lipgloss.Style

staticFooter string

rows []Row

selectableRows bool
Expand All @@ -32,6 +34,8 @@ type Model struct {
selectedRows []Row

border Border

totalWidth int
}

// New creates a new table ready for further modifications.
Expand All @@ -46,6 +50,8 @@ func New(columns []Column) Model {
// Do a full deep copy to avoid unexpected edits
copy(model.columns, columns)

model.recalculateWidth()

return model
}

Expand Down
9 changes: 9 additions & 0 deletions table/options.go
Expand Up @@ -46,6 +46,8 @@ func (m Model) SelectableRows(selectable bool) Model {
}
}

m.recalculateWidth()

return m
}

Expand Down Expand Up @@ -79,3 +81,10 @@ func (m Model) Focused(focused bool) Model {

return m
}

// WithStaticFooter adds a footer that only displays the given text.
func (m Model) WithStaticFooter(footer string) Model {
m.staticFooter = footer

return m
}
6 changes: 6 additions & 0 deletions table/view.go
Expand Up @@ -45,6 +45,12 @@ func (m Model) View() string {
rowStrs = append(rowStrs, m.renderRow(i))
}

footer := m.renderFooter()

if footer != "" {
rowStrs = append(rowStrs, footer)
}

body.WriteString(lipgloss.JoinVertical(lipgloss.Left, rowStrs...))

return body.String()
Expand Down
93 changes: 93 additions & 0 deletions table/view_test.go
Expand Up @@ -125,3 +125,96 @@ func TestSimple3x2(t *testing.T) {

assert.Equal(t, expectedTable, rendered)
}

func TestSingleHeaderWithFooter(t *testing.T) {
model := New([]Column{
NewColumn("id", "ID", 4),
}).WithStaticFooter("Foot")

const expectedTable = `┏━━━━┓
┃ ID┃
┣━━━━┫
┃Foot┃
┗━━━━┛`
rendered := model.View()

assert.Equal(t, expectedTable, rendered)
}

func TestSingleRowWithFooterView(t *testing.T) {
model := New([]Column{
NewColumn("1", "1", 4),
NewColumn("2", "2", 4),
NewColumn("3", "3", 4),
}).WithStaticFooter("Footer")

const expectedTable = `┏━━━━┳━━━━┳━━━━┓
┃ 1┃ 2┃ 3┃
┣━━━━┻━━━━┻━━━━┫
┃ Footer┃
┗━━━━━━━━━━━━━━┛`

rendered := model.View()

assert.Equal(t, expectedTable, rendered)
}

func TestSingleColumnWithFooterView(t *testing.T) {
model := New([]Column{
NewColumn("id", "ID", 4),
}).WithRows([]Row{
NewRow(RowData{"id": "1"}),
NewRow(RowData{"id": "2"}),
}).WithStaticFooter("Foot")

const expectedTable = `┏━━━━┓
┃ ID┃
┣━━━━┫
┃ 1┃
┃ 2┃
┣━━━━┫
┃Foot┃
┗━━━━┛`

rendered := model.View()

assert.Equal(t, expectedTable, rendered)
}

func TestSimple3x2WithFooterView(t *testing.T) {
model := New([]Column{
NewColumn("1", "1", 4),
NewColumn("2", "2", 4),
NewColumn("3", "3", 4),
})

rows := []Row{}

for rowIndex := 1; rowIndex <= 3; rowIndex++ {
rowData := RowData{}

for columnIndex := 1; columnIndex <= 3; columnIndex++ {
id := fmt.Sprintf("%d", columnIndex)

rowData[id] = fmt.Sprintf("%d,%d", columnIndex, rowIndex)
}

rows = append(rows, NewRow(rowData))
}

model = model.WithRows(rows).WithStaticFooter("Footer")

const expectedTable = `┏━━━━┳━━━━┳━━━━┓
┃ 1┃ 2┃ 3┃
┣━━━━╋━━━━╋━━━━┫
┃ 1,1┃ 2,1┃ 3,1┃
┃ 1,2┃ 2,2┃ 3,2┃
┃ 1,3┃ 2,3┃ 3,3┃
┣━━━━┻━━━━┻━━━━┫
┃ Footer┃
┗━━━━━━━━━━━━━━┛`

rendered := model.View()

assert.Equal(t, expectedTable, rendered)
}

0 comments on commit cc947b2

Please sign in to comment.