lipgloss
lipgloss copied to clipboard
Improper rendering of lipgloss.Table
Describe the bug
The lipgloss.Table rendering provides improper rendering in a few ways:
- Headers are inconsistently rendered outside of the size of the window
- Multiple rows are rendered with the same style during an update
- The bottom of the table will scroll to the top of the window
Setup Please complete the following information along with version numbers, if applicable.
- macOS Sequoia
- ZSH, default from macOS
- Observed in both
rio&iterm - No multiplexer
en_US.UTF-8, default for macOS.
To Reproduce Steps to reproduce the behavior:
- Render the table with more rows than the height of the window.
Unfortunately, I can't share the code that's rendering the table, only the table component itself. You can render the table as the only thing rendered and still observe this behavior
Source Code
Here is the table component I built. It's pulled from github.com/charmbracelet/bubbles.Table, but modified to use lipgloss.Table instead.
package table
import (
"github.com/charmbracelet/bubbles/help"
"github.com/charmbracelet/bubbles/key"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/lipgloss/table"
"github.com/jamf/k8s-hermes-cli/internal/styles"
"github.com/jamf/k8s-hermes-cli/internal/utils"
jamfcolor "github.com/jamf/pkg/styles"
)
// table constants
const (
headerRow int = 0
firstRow int = 1
)
type Data interface {
table.Data
Headers() []string
}
var _ tea.Model = (*Model)(nil)
// KeyMap defines keybindings for the Model.
type KeyMap struct {
LineUp key.Binding
LineDown key.Binding
PageUp key.Binding
PageDown key.Binding
HalfPageUp key.Binding
HalfPageDown key.Binding
GotoTop key.Binding
GotoBottom key.Binding
}
// ShortHelp implements the KeyMap interface.
func (km KeyMap) ShortHelp() []key.Binding {
return []key.Binding{km.LineUp, km.LineDown}
}
// FullHelp implements the KeyMap interface.
func (km KeyMap) FullHelp() [][]key.Binding {
return [][]key.Binding{
{km.LineUp, km.LineDown, km.GotoTop, km.GotoBottom},
{km.PageUp, km.PageDown, km.HalfPageUp, km.HalfPageDown},
}
}
// DefaultKeyMap returns a default set of keybindings.
func DefaultKeyMap() KeyMap {
const spacebar = " "
return KeyMap{
LineUp: key.NewBinding(
key.WithKeys("up", "k"),
key.WithHelp("↑/k", "up"),
),
LineDown: key.NewBinding(
key.WithKeys("down", "j"),
key.WithHelp("↓/j", "down"),
),
PageUp: key.NewBinding(
key.WithKeys("b", "pgup"),
key.WithHelp("b/pgup", "page up"),
),
PageDown: key.NewBinding(
key.WithKeys("f", "pgdown", spacebar),
key.WithHelp("f/pgdn", "page down"),
),
HalfPageUp: key.NewBinding(
key.WithKeys("u", "ctrl+u"),
key.WithHelp("u", "½ page up"),
),
HalfPageDown: key.NewBinding(
key.WithKeys("d", "ctrl+d"),
key.WithHelp("d", "½ page down"),
),
GotoTop: key.NewBinding(
key.WithKeys("home", "g"),
key.WithHelp("g/home", "go to start"),
),
GotoBottom: key.NewBinding(
key.WithKeys("end", "G"),
key.WithHelp("G/end", "go to end"),
),
}
}
type Styles struct {
Border lipgloss.Border
BorderStyle lipgloss.Style
BorderHeader bool
Header lipgloss.Style
Cell lipgloss.Style
Selected lipgloss.Style
}
func DefaultStyles() Styles {
return Styles{
Border: lipgloss.RoundedBorder(),
BorderHeader: true,
BorderStyle: styles.BaseStyle().BorderForeground(lipgloss.Color(jamfcolor.AthensGrey)),
Selected: styles.BaseStyle().Bold(true).Background(lipgloss.Color(jamfcolor.Havelock)),
Header: styles.BaseStyle().Bold(true).Padding(1).AlignHorizontal(lipgloss.Center),
Cell: styles.BaseStyle().Padding(1),
}
}
type Option func(*Model)
type Model struct {
KeyMap KeyMap
Help help.Model
yoffset int
height int
data Data
cursor int
focus bool
styles Styles
table *table.Table
start int
end int
}
func New(opts ...Option) *Model {
m := &Model{
cursor: firstRow,
table: table.New(),
KeyMap: DefaultKeyMap(),
Help: help.New(),
styles: DefaultStyles(),
}
m.Help.ShowAll = true
for _, o := range opts {
o(m)
}
return m
}
// WithHeaders sets the headers for the table.
func WithHeaders(headers ...string) Option {
return func(m *Model) {
m.table.Headers(headers...)
}
}
// WithRows sets the rows for the table.
func WithRows(rows ...[]string) Option {
return func(m *Model) {
m.table.Rows(rows...)
}
}
// WithData sets the table data.
func WithData(data table.Data) Option {
return func(m *Model) {
m.table = m.table.Data(data)
}
}
func WithFocus() Option {
return func(m *Model) {
m.focus = true
}
}
func WithStyles(styles Styles) Option {
return func(m *Model) {
m.styles = styles
}
}
func WithKeyMap(km KeyMap) Option {
return func(m *Model) {
m.KeyMap = km
}
}
func WithHeight(h int) Option {
return func(m *Model) {
m.height = h
m.table.Height(h)
}
}
func WithWidth(w int) Option {
return func(m *Model) {
m.table.Width(w)
}
}
// SetData sets the table data.
func (m *Model) SetData(data Data) {
m.data = data
m.table = m.table.Data(data)
}
// SetHeaders sets the table headers.
func (m *Model) SetHeaders(headers ...string) {
m.table = m.table.Headers(headers...)
}
func (m *Model) Cursor() int {
return m.cursor
}
func (m *Model) SetCursor(n int) {
m.cursor = utils.Clamp(n, 0, m.data.Rows())
}
// MoveUp moves the cursor up n rows, up to the first row.
func (m *Model) MoveUp(n int) {
m.SetCursor(m.cursor - n)
m.setYOffset(m.yoffset - n)
m.table.Offset(m.yoffset)
}
// MoveDown moves the cursor down n rows, up to the last row.
func (m *Model) MoveDown(n int) {
m.SetCursor(m.cursor + n)
m.setYOffset(m.yoffset + n)
m.table.Offset(m.yoffset)
}
func (m *Model) GoToBottom() {
m.MoveDown(m.data.Rows())
}
func (m *Model) GoToTop() {
// todo (sienna): this feels buggy
m.MoveUp(firstRow)
}
func (m *Model) Height() int {
return m.height
}
func (m *Model) SetHeight(h int) {
m.height = h
m.table = m.table.Height(h)
}
// SetWidth sets the width of the table.
func (m *Model) SetWidth(w int) {
m.table = m.table.Width(w)
}
// Focused returns true if the table is focused.
func (m *Model) Focused() bool {
return m.focus
}
// Focus focuses the table, allowing the user to move around the rows and
// interact.
func (m *Model) Focus() {
m.focus = true
}
// Blur blurs the table, preventing selection or movement.
func (m *Model) Blur() {
m.focus = false
}
func (m *Model) HelpView() string {
return m.Help.View(m.KeyMap)
}
func (m *Model) setYOffset(n int) {
m.yoffset = utils.Clamp(n, 0, m.data.Rows())
}
func (m *Model) Init() tea.Cmd {
return nil
}
func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if !m.focus {
return m, nil
}
switch msg := msg.(type) {
case tea.KeyMsg:
switch {
case key.Matches(msg, m.KeyMap.LineUp):
m.MoveUp(1)
case key.Matches(msg, m.KeyMap.LineDown):
m.MoveDown(1)
case key.Matches(msg, m.KeyMap.PageUp):
m.MoveUp(m.height)
case key.Matches(msg, m.KeyMap.PageDown):
m.MoveDown(m.height)
case key.Matches(msg, m.KeyMap.HalfPageUp):
m.MoveUp(m.height / 2)
case key.Matches(msg, m.KeyMap.HalfPageDown):
m.MoveDown(m.height / 2)
case key.Matches(msg, m.KeyMap.LineDown):
m.MoveDown(1)
case key.Matches(msg, m.KeyMap.GotoTop):
m.GoToTop()
case key.Matches(msg, m.KeyMap.GotoBottom):
m.GoToBottom()
}
case tea.MouseMsg:
if msg.Button == tea.MouseButtonWheelUp {
m.MoveUp(1)
}
if msg.Button == tea.MouseButtonWheelDown {
m.MoveDown(1)
}
}
return m, nil
}
func (m *Model) View() string {
m.table.StyleFunc(func(row int, col int) lipgloss.Style {
if row == headerRow {
return m.styles.Header
}
if row == m.cursor {
return m.styles.Selected
}
return m.styles.Cell
})
return m.table.String()
}
Expected behavior
- Headers should render at the top of the window.
StyleFuncshould consistently highlight rows- The table should not scroll up to the top of the window but remain locked to the bottom.
Screenshots
https://github.com/user-attachments/assets/19d90fc2-0047-4935-87e1-ae44b7897944
Additional context
I've tested several different iterations and still end up with the same generalized results.
Hey, I think this should be fixed by https://github.com/charmbracelet/lipgloss/pull/373 We have an open PR in bubbles as well to make it render using Lip Gloss table as well if that's of interest! https://github.com/charmbracelet/bubbles/pull/617
If you have any feedback on that one, please don't hesitate to let us know
This might be fixed by https://github.com/charmbracelet/lipgloss/pull/479