253 lines
6.2 KiB
Go
253 lines
6.2 KiB
Go
package table
|
|
|
|
import (
|
|
"fmt"
|
|
"sync/atomic"
|
|
|
|
"github.com/charmbracelet/lipgloss"
|
|
"github.com/muesli/reflow/wordwrap"
|
|
)
|
|
|
|
// RowData is a map of string column keys to arbitrary data. Data with a key
|
|
// that matches a column key will be displayed. Data with a key that does not
|
|
// match a column key will not be displayed, but will remain attached to the Row.
|
|
// This can be useful for attaching hidden metadata for future reference when
|
|
// retrieving rows.
|
|
type RowData map[string]any
|
|
|
|
// Row represents a row in the table with some data keyed to the table columns>
|
|
// Can have a style applied to it such as color/bold. Create using NewRow().
|
|
type Row struct {
|
|
Style lipgloss.Style
|
|
Data RowData
|
|
|
|
selected bool
|
|
|
|
// id is an internal unique ID to match rows after they're copied
|
|
id uint32
|
|
}
|
|
|
|
var lastRowID uint32 = 1
|
|
|
|
// NewRow creates a new row and copies the given row data.
|
|
func NewRow(data RowData) Row {
|
|
row := Row{
|
|
Data: make(map[string]any),
|
|
id: lastRowID,
|
|
}
|
|
|
|
atomic.AddUint32(&lastRowID, 1)
|
|
|
|
for key, val := range data {
|
|
// Doesn't deep copy val, but close enough for now...
|
|
row.Data[key] = val
|
|
}
|
|
|
|
return row
|
|
}
|
|
|
|
// WithStyle uses the given style for the text in the row.
|
|
func (r Row) WithStyle(style lipgloss.Style) Row {
|
|
r.Style = style.Copy()
|
|
|
|
return r
|
|
}
|
|
|
|
//nolint:cyclop,funlen // Breaking this up will be more complicated than it's worth for now
|
|
func (m Model) renderRowColumnData(row Row, column Column, rowStyle lipgloss.Style, borderStyle lipgloss.Style) string {
|
|
cellStyle := rowStyle.Copy().Inherit(column.style).Inherit(m.baseStyle)
|
|
|
|
var str string
|
|
|
|
switch column.key {
|
|
case columnKeySelect:
|
|
if row.selected {
|
|
str = m.selectedText
|
|
} else {
|
|
str = m.unselectedText
|
|
}
|
|
case columnKeyOverflowRight:
|
|
cellStyle = cellStyle.Align(lipgloss.Right)
|
|
str = ">"
|
|
case columnKeyOverflowLeft:
|
|
str = "<"
|
|
default:
|
|
fmtString := "%v"
|
|
|
|
var data any
|
|
|
|
if entry, exists := row.Data[column.key]; exists {
|
|
data = entry
|
|
|
|
if column.fmtString != "" {
|
|
fmtString = column.fmtString
|
|
}
|
|
} else if m.missingDataIndicator != nil {
|
|
data = m.missingDataIndicator
|
|
} else {
|
|
data = ""
|
|
}
|
|
|
|
switch entry := data.(type) {
|
|
case StyledCell:
|
|
str = fmt.Sprintf(fmtString, entry.Data)
|
|
|
|
if entry.StyleFunc != nil {
|
|
cellStyle = entry.StyleFunc(StyledCellFuncInput{
|
|
Column: column,
|
|
Data: entry.Data,
|
|
Row: row,
|
|
GlobalMetadata: m.metadata,
|
|
}).Copy().Inherit(cellStyle)
|
|
} else {
|
|
cellStyle = entry.Style.Copy().Inherit(cellStyle)
|
|
}
|
|
default:
|
|
str = fmt.Sprintf(fmtString, entry)
|
|
}
|
|
}
|
|
|
|
if m.multiline {
|
|
str = wordwrap.String(str, column.width)
|
|
cellStyle = cellStyle.Align(lipgloss.Top)
|
|
} else {
|
|
str = limitStr(str, column.width)
|
|
}
|
|
|
|
cellStyle = cellStyle.Inherit(borderStyle)
|
|
cellStr := cellStyle.Render(str)
|
|
|
|
return cellStr
|
|
}
|
|
|
|
func (m Model) renderRow(rowIndex int, last bool) string {
|
|
row := m.GetVisibleRows()[rowIndex]
|
|
highlighted := rowIndex == m.rowCursorIndex
|
|
|
|
rowStyle := row.Style.Copy()
|
|
|
|
if m.rowStyleFunc != nil {
|
|
styleResult := m.rowStyleFunc(RowStyleFuncInput{
|
|
Index: rowIndex,
|
|
Row: row,
|
|
IsHighlighted: m.focused && highlighted,
|
|
})
|
|
|
|
rowStyle = rowStyle.Inherit(styleResult)
|
|
} else if m.focused && highlighted {
|
|
rowStyle = rowStyle.Inherit(m.highlightStyle)
|
|
}
|
|
|
|
return m.renderRowData(row, rowStyle, last)
|
|
}
|
|
|
|
func (m Model) renderBlankRow(last bool) string {
|
|
return m.renderRowData(NewRow(nil), lipgloss.NewStyle(), last)
|
|
}
|
|
|
|
// This is long and could use some refactoring in the future, but not quite sure
|
|
// how to pick it apart yet.
|
|
//
|
|
//nolint:funlen, cyclop
|
|
func (m Model) renderRowData(row Row, rowStyle lipgloss.Style, last bool) string {
|
|
numColumns := len(m.columns)
|
|
|
|
columnStrings := []string{}
|
|
totalRenderedWidth := 0
|
|
|
|
stylesInner, stylesLast := m.styleRows()
|
|
|
|
maxCellHeight := 1
|
|
if m.multiline {
|
|
for _, column := range m.columns {
|
|
cellStr := m.renderRowColumnData(row, column, rowStyle, lipgloss.NewStyle())
|
|
maxCellHeight = max(maxCellHeight, lipgloss.Height(cellStr))
|
|
}
|
|
}
|
|
|
|
for columnIndex, column := range m.columns {
|
|
var borderStyle lipgloss.Style
|
|
var rowStyles borderStyleRow
|
|
|
|
if !last {
|
|
rowStyles = stylesInner
|
|
} else {
|
|
rowStyles = stylesLast
|
|
}
|
|
rowStyle = rowStyle.Copy().Height(maxCellHeight)
|
|
|
|
if m.horizontalScrollOffsetCol > 0 && columnIndex == m.horizontalScrollFreezeColumnsCount {
|
|
var borderStyle lipgloss.Style
|
|
|
|
if columnIndex == 0 {
|
|
borderStyle = rowStyles.left.Copy()
|
|
} else {
|
|
borderStyle = rowStyles.inner.Copy()
|
|
}
|
|
|
|
rendered := m.renderRowColumnData(row, genOverflowColumnLeft(1), rowStyle, borderStyle)
|
|
|
|
totalRenderedWidth += lipgloss.Width(rendered)
|
|
|
|
columnStrings = append(columnStrings, rendered)
|
|
}
|
|
|
|
if columnIndex >= m.horizontalScrollFreezeColumnsCount &&
|
|
columnIndex < m.horizontalScrollOffsetCol+m.horizontalScrollFreezeColumnsCount {
|
|
continue
|
|
}
|
|
|
|
if len(columnStrings) == 0 {
|
|
borderStyle = rowStyles.left
|
|
} else if columnIndex < numColumns-1 {
|
|
borderStyle = rowStyles.inner
|
|
} else {
|
|
borderStyle = rowStyles.right
|
|
}
|
|
|
|
cellStr := m.renderRowColumnData(row, column, rowStyle, borderStyle)
|
|
|
|
if m.maxTotalWidth != 0 {
|
|
renderedWidth := lipgloss.Width(cellStr)
|
|
|
|
const (
|
|
borderAdjustment = 1
|
|
overflowColWidth = 2
|
|
)
|
|
|
|
targetWidth := m.maxTotalWidth - overflowColWidth
|
|
|
|
if columnIndex == len(m.columns)-1 {
|
|
// If this is the last header, we don't need to account for the
|
|
// overflow arrow column
|
|
targetWidth = m.maxTotalWidth
|
|
}
|
|
|
|
if totalRenderedWidth+renderedWidth > targetWidth {
|
|
overflowWidth := m.maxTotalWidth - totalRenderedWidth - borderAdjustment
|
|
overflowStyle := genOverflowStyle(rowStyles.right, overflowWidth)
|
|
overflowColumn := genOverflowColumnRight(overflowWidth)
|
|
overflowStr := m.renderRowColumnData(row, overflowColumn, rowStyle, overflowStyle)
|
|
|
|
columnStrings = append(columnStrings, overflowStr)
|
|
|
|
break
|
|
}
|
|
|
|
totalRenderedWidth += renderedWidth
|
|
}
|
|
|
|
columnStrings = append(columnStrings, cellStr)
|
|
}
|
|
|
|
return lipgloss.JoinHorizontal(lipgloss.Bottom, columnStrings...)
|
|
}
|
|
|
|
// Selected returns a copy of the row that's set to be selected or deselected.
|
|
// The old row is not changed in-place.
|
|
func (r Row) Selected(selected bool) Row {
|
|
r.selected = selected
|
|
|
|
return r
|
|
}
|