chore: go mod tidy / vendor / make deps

This commit is contained in:
2025-10-02 08:25:31 +02:00
parent 1c10e64c58
commit d63a1c28ea
505 changed files with 34448 additions and 35285 deletions

21
vendor/github.com/evertras/bubble-table/LICENSE generated vendored Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Brandon Fulljames
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

439
vendor/github.com/evertras/bubble-table/table/border.go generated vendored Normal file
View File

@ -0,0 +1,439 @@
package table
import "github.com/charmbracelet/lipgloss"
// Border defines the borders in and around the table.
type Border struct {
Top string
Left string
Right string
Bottom string
TopRight string
TopLeft string
BottomRight string
BottomLeft string
TopJunction string
LeftJunction string
RightJunction string
BottomJunction string
InnerJunction string
InnerDivider string
// Styles for 2x2 tables and larger
styleMultiTopLeft lipgloss.Style
styleMultiTop lipgloss.Style
styleMultiTopRight lipgloss.Style
styleMultiRight lipgloss.Style
styleMultiBottomRight lipgloss.Style
styleMultiBottom lipgloss.Style
styleMultiBottomLeft lipgloss.Style
styleMultiLeft lipgloss.Style
styleMultiInner lipgloss.Style
// Styles for a single column table
styleSingleColumnTop lipgloss.Style
styleSingleColumnInner lipgloss.Style
styleSingleColumnBottom lipgloss.Style
// Styles for a single row table
styleSingleRowLeft lipgloss.Style
styleSingleRowInner lipgloss.Style
styleSingleRowRight lipgloss.Style
// Style for a table with only one cell
styleSingleCell lipgloss.Style
// Style for the footer
styleFooter lipgloss.Style
}
var (
// https://www.w3.org/TR/xml-entity-names/025.html
borderDefault = Border{
Top: "━",
Left: "┃",
Right: "┃",
Bottom: "━",
TopRight: "┓",
TopLeft: "┏",
BottomRight: "┛",
BottomLeft: "┗",
TopJunction: "┳",
LeftJunction: "┣",
RightJunction: "┫",
BottomJunction: "┻",
InnerJunction: "╋",
InnerDivider: "┃",
}
borderRounded = Border{
Top: "─",
Left: "│",
Right: "│",
Bottom: "─",
TopRight: "╮",
TopLeft: "╭",
BottomRight: "╯",
BottomLeft: "╰",
TopJunction: "┬",
LeftJunction: "├",
RightJunction: "┤",
BottomJunction: "┴",
InnerJunction: "┼",
InnerDivider: "│",
}
)
func init() {
borderDefault.generateStyles()
borderRounded.generateStyles()
}
func (b *Border) generateStyles() {
b.generateMultiStyles()
b.generateSingleColumnStyles()
b.generateSingleRowStyles()
b.generateSingleCellStyle()
// The footer is a single cell with the top taken off... usually. We can
// re-enable the top if needed this way for certain format configurations.
b.styleFooter = b.styleSingleCell.Copy().
Align(lipgloss.Right).
BorderBottom(true).
BorderRight(true).
BorderLeft(true)
}
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...
//
//nolint:funlen
func (b *Border) generateMultiStyles() {
b.styleMultiTopLeft = lipgloss.NewStyle().BorderStyle(
lipgloss.Border{
TopLeft: b.TopLeft,
Top: b.Top,
TopRight: b.TopJunction,
Right: b.InnerDivider,
BottomRight: b.InnerJunction,
Bottom: b.Bottom,
BottomLeft: b.LeftJunction,
Left: b.Left,
},
)
b.styleMultiTop = lipgloss.NewStyle().BorderStyle(
lipgloss.Border{
Top: b.Top,
Right: b.InnerDivider,
Bottom: b.Bottom,
TopRight: b.TopJunction,
BottomRight: b.InnerJunction,
},
).BorderTop(true).BorderBottom(true).BorderRight(true)
b.styleMultiTopRight = lipgloss.NewStyle().BorderStyle(
lipgloss.Border{
Top: b.Top,
Right: b.Right,
Bottom: b.Bottom,
TopRight: b.TopRight,
BottomRight: b.RightJunction,
},
).BorderTop(true).BorderBottom(true).BorderRight(true)
b.styleMultiLeft = lipgloss.NewStyle().BorderStyle(
lipgloss.Border{
Left: b.Left,
Right: b.InnerDivider,
},
).BorderRight(true).BorderLeft(true)
b.styleMultiRight = lipgloss.NewStyle().BorderStyle(
lipgloss.Border{
Right: b.Right,
},
).BorderRight(true)
b.styleMultiInner = lipgloss.NewStyle().BorderStyle(
lipgloss.Border{
Right: b.InnerDivider,
},
).BorderRight(true)
b.styleMultiBottomLeft = lipgloss.NewStyle().BorderStyle(
lipgloss.Border{
Left: b.Left,
Right: b.InnerDivider,
Bottom: b.Bottom,
BottomLeft: b.BottomLeft,
BottomRight: b.BottomJunction,
},
).BorderLeft(true).BorderBottom(true).BorderRight(true)
b.styleMultiBottom = lipgloss.NewStyle().BorderStyle(
lipgloss.Border{
Right: b.InnerDivider,
Bottom: b.Bottom,
BottomRight: b.BottomJunction,
},
).BorderBottom(true).BorderRight(true)
b.styleMultiBottomRight = lipgloss.NewStyle().BorderStyle(
lipgloss.Border{
Right: b.Right,
Bottom: b.Bottom,
BottomRight: b.BottomRight,
},
).BorderBottom(true).BorderRight(true)
}
func (b *Border) generateSingleColumnStyles() {
b.styleSingleColumnTop = lipgloss.NewStyle().BorderStyle(
lipgloss.Border{
Top: b.Top,
Left: b.Left,
Right: b.Right,
Bottom: b.Bottom,
TopLeft: b.TopLeft,
TopRight: b.TopRight,
BottomLeft: b.LeftJunction,
BottomRight: b.RightJunction,
},
)
b.styleSingleColumnInner = lipgloss.NewStyle().BorderStyle(
lipgloss.Border{
Left: b.Left,
Right: b.Right,
},
).BorderRight(true).BorderLeft(true)
b.styleSingleColumnBottom = lipgloss.NewStyle().BorderStyle(
lipgloss.Border{
Left: b.Left,
Right: b.Right,
Bottom: b.Bottom,
BottomLeft: b.BottomLeft,
BottomRight: b.BottomRight,
},
).BorderRight(true).BorderLeft(true).BorderBottom(true)
}
func (b *Border) generateSingleRowStyles() {
b.styleSingleRowLeft = lipgloss.NewStyle().BorderStyle(
lipgloss.Border{
Top: b.Top,
Left: b.Left,
Right: b.InnerDivider,
Bottom: b.Bottom,
BottomLeft: b.BottomLeft,
BottomRight: b.BottomJunction,
TopRight: b.TopJunction,
TopLeft: b.TopLeft,
},
)
b.styleSingleRowInner = lipgloss.NewStyle().BorderStyle(
lipgloss.Border{
Top: b.Top,
Right: b.InnerDivider,
Bottom: b.Bottom,
BottomRight: b.BottomJunction,
TopRight: b.TopJunction,
},
).BorderTop(true).BorderBottom(true).BorderRight(true)
b.styleSingleRowRight = lipgloss.NewStyle().BorderStyle(
lipgloss.Border{
Top: b.Top,
Right: b.Right,
Bottom: b.Bottom,
BottomRight: b.BottomRight,
TopRight: b.TopRight,
},
).BorderTop(true).BorderBottom(true).BorderRight(true)
}
func (b *Border) generateSingleCellStyle() {
b.styleSingleCell = lipgloss.NewStyle().BorderStyle(
lipgloss.Border{
Top: b.Top,
Left: b.Left,
Right: b.Right,
Bottom: b.Bottom,
BottomLeft: b.BottomLeft,
BottomRight: b.BottomRight,
TopRight: b.TopRight,
TopLeft: b.TopLeft,
},
)
}
// BorderDefault uses the basic square border, useful to reset the border if
// it was changed somehow.
func (m Model) BorderDefault() Model {
// Already generated styles
m.border = borderDefault
return m
}
// BorderRounded uses a thin, rounded border.
func (m Model) BorderRounded() Model {
// Already generated styles
m.border = borderRounded
return m
}
// Border uses the given border components to render the table.
func (m Model) Border(border Border) Model {
border.generateStyles()
m.border = border
return m
}
type borderStyleRow struct {
left lipgloss.Style
inner lipgloss.Style
right lipgloss.Style
}
func (b *borderStyleRow) inherit(s lipgloss.Style) {
b.left = b.left.Copy().Inherit(s)
b.inner = b.inner.Copy().Inherit(s)
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.GetVisibleRows()) > 0 || m.calculatePadding(0) > 0
singleColumn := len(m.columns) == 1
styles := borderStyleRow{}
// Possible configurations:
// - Single cell
// - Single row
// - Single column
// - Multi
if singleColumn {
if hasRows {
// Single column
styles.left = m.border.styleSingleColumnTop
styles.inner = styles.left
styles.right = styles.left
} else {
// Single cell
styles.left = 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
styles.inner = m.border.styleMultiTop
styles.right = m.border.styleMultiTopRight
}
styles.inherit(m.headerStyle)
return styles
}
func (m Model) styleRows() (inner borderStyleRow, last borderStyleRow) {
if len(m.columns) == 1 {
inner.left = m.border.styleSingleColumnInner
inner.inner = inner.left
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 {
inner.left = m.border.styleMultiLeft
inner.inner = m.border.styleMultiInner
inner.right = m.border.styleMultiRight
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
}

36
vendor/github.com/evertras/bubble-table/table/calc.go generated vendored Normal file
View File

@ -0,0 +1,36 @@
package table
// Keep compatibility with Go 1.21 by re-declaring min.
//
//nolint:predeclared
func min(x, y int) int {
if x < y {
return x
}
return y
}
// Keep compatibility with Go 1.21 by re-declaring max.
//
//nolint:predeclared
func max(x, y int) int {
if x > y {
return x
}
return y
}
// These var names are fine for this little function
//
//nolint:varnamelen
func gcd(x, y int) int {
if x == 0 {
return y
} else if y == 0 {
return x
}
return gcd(y%x, x)
}

60
vendor/github.com/evertras/bubble-table/table/cell.go generated vendored Normal file
View File

@ -0,0 +1,60 @@
package table
import "github.com/charmbracelet/lipgloss"
// StyledCell represents a cell in the table that has a particular style applied.
// The cell style takes highest precedence and will overwrite more general styles
// from the row, column, or table as a whole. This style should be generally
// limited to colors, font style, and alignments - spacing style such as margin
// will break the table format.
type StyledCell struct {
// Data is the content of the cell.
Data any
// Style is the specific style to apply. This is ignored if StyleFunc is not nil.
Style lipgloss.Style
// StyleFunc is a function that takes the row/column of the cell and
// returns a lipgloss.Style allowing for dynamic styling based on the cell's
// content or position. Overrides Style if set.
StyleFunc StyledCellFunc
}
// StyledCellFuncInput is the input to the StyledCellFunc. Sent as a struct
// to allow for future additions without breaking changes.
type StyledCellFuncInput struct {
// Data is the data in the cell.
Data any
// Column is the column that the cell belongs to.
Column Column
// Row is the row that the cell belongs to.
Row Row
// GlobalMetadata is the global table metadata that's been set by WithGlobalMetadata
GlobalMetadata map[string]any
}
// StyledCellFunc is a function that takes various information about the cell and
// returns a lipgloss.Style allowing for easier dynamic styling based on the cell's
// content or position.
type StyledCellFunc = func(input StyledCellFuncInput) lipgloss.Style
// NewStyledCell creates an entry that can be set in the row data and show as
// styled with the given style.
func NewStyledCell(data any, style lipgloss.Style) StyledCell {
return StyledCell{
Data: data,
Style: style,
}
}
// NewStyledCellWithStyleFunc creates an entry that can be set in the row data and show as
// styled with the given style function.
func NewStyledCellWithStyleFunc(data any, styleFunc StyledCellFunc) StyledCell {
return StyledCell{
Data: data,
StyleFunc: styleFunc,
}
}

118
vendor/github.com/evertras/bubble-table/table/column.go generated vendored Normal file
View File

@ -0,0 +1,118 @@
package table
import (
"github.com/charmbracelet/lipgloss"
)
// Column is a column in the table.
type Column struct {
title string
key string
width int
flexFactor int
filterable bool
style lipgloss.Style
fmtString string
}
// NewColumn creates a new fixed-width column with the given information.
func NewColumn(key, title string, width int) Column {
return Column{
key: key,
title: title,
width: width,
filterable: false,
}
}
// NewFlexColumn creates a new flexible width column that tries to fill in the
// total table width. If multiple flex columns exist, each will measure against
// each other depending on their flexFactor. For example, if both have a flexFactor
// of 1, they will have equal width. If one has a flexFactor of 1 and the other
// has a flexFactor of 3, the second will be 3 times larger than the first. You
// must use WithTargetWidth if you have any flex columns, so that the table knows
// how much width it should fill.
func NewFlexColumn(key, title string, flexFactor int) Column {
return Column{
key: key,
title: title,
flexFactor: max(flexFactor, 1),
}
}
// WithStyle applies a style to the column as a whole.
func (c Column) WithStyle(style lipgloss.Style) Column {
c.style = style.Copy().Width(c.width)
return c
}
// WithFiltered sets whether the column should be considered for filtering (true)
// or not (false).
func (c Column) WithFiltered(filterable bool) Column {
c.filterable = filterable
return c
}
// WithFormatString sets the format string used by fmt.Sprintf to display the data.
// If not set, the default is "%v" for all data types. Intended mainly for
// numeric formatting.
//
// Since data is of the any type, make sure that all data in the column
// is of the expected type or the format may fail. For example, hardcoding '3'
// instead of '3.0' and using '%.2f' will fail because '3' is an integer.
func (c Column) WithFormatString(fmtString string) Column {
c.fmtString = fmtString
return c
}
func (c *Column) isFlex() bool {
return c.flexFactor != 0
}
// Title returns the title of the column.
func (c Column) Title() string {
return c.title
}
// Key returns the key of the column.
func (c Column) Key() string {
return c.key
}
// Width returns the width of the column.
func (c Column) Width() int {
return c.width
}
// FlexFactor returns the flex factor of the column.
func (c Column) FlexFactor() int {
return c.flexFactor
}
// IsFlex returns whether the column is a flex column.
func (c Column) IsFlex() bool {
return c.isFlex()
}
// Filterable returns whether the column is filterable.
func (c Column) Filterable() bool {
return c.filterable
}
// Style returns the style of the column.
func (c Column) Style() lipgloss.Style {
return c.style
}
// FmtString returns the format string of the column.
func (c Column) FmtString() string {
return c.fmtString
}

67
vendor/github.com/evertras/bubble-table/table/data.go generated vendored Normal file
View File

@ -0,0 +1,67 @@
package table
import "time"
// This is just a bunch of data type checks, so... no linting here
//
//nolint:cyclop
func asInt(data any) (int64, bool) {
switch val := data.(type) {
case int:
return int64(val), true
case int8:
return int64(val), true
case int16:
return int64(val), true
case int32:
return int64(val), true
case int64:
return val, true
case uint:
// #nosec: G115
return int64(val), true
case uint8:
return int64(val), true
case uint16:
return int64(val), true
case uint32:
return int64(val), true
case uint64:
// #nosec: G115
return int64(val), true
case time.Duration:
return int64(val), true
case StyledCell:
return asInt(val.Data)
}
return 0, false
}
func asNumber(data any) (float64, bool) {
switch val := data.(type) {
case float32:
return float64(val), true
case float64:
return val, true
case StyledCell:
return asNumber(val.Data)
}
intVal, isInt := asInt(data)
return float64(intVal), isInt
}

View File

@ -0,0 +1,116 @@
package table
import (
"github.com/charmbracelet/lipgloss"
)
func (m *Model) recalculateWidth() {
if m.targetTotalWidth != 0 {
m.totalWidth = m.targetTotalWidth
} else {
total := 0
for _, column := range m.columns {
total += column.width
}
m.totalWidth = total + len(m.columns) + 1
}
updateColumnWidths(m.columns, m.targetTotalWidth)
m.recalculateLastHorizontalColumn()
}
// Updates column width in-place. This could be optimized but should be called
// very rarely so we prioritize simplicity over performance here.
func updateColumnWidths(cols []Column, totalWidth int) {
totalFlexWidth := totalWidth - len(cols) - 1
totalFlexFactor := 0
flexGCD := 0
for index, col := range cols {
if !col.isFlex() {
totalFlexWidth -= col.width
cols[index].style = col.style.Width(col.width)
} else {
totalFlexFactor += col.flexFactor
flexGCD = gcd(flexGCD, col.flexFactor)
}
}
if totalFlexFactor == 0 {
return
}
// We use the GCD here because otherwise very large values won't divide
// nicely as ints
totalFlexFactor /= flexGCD
flexUnit := totalFlexWidth / totalFlexFactor
leftoverWidth := totalFlexWidth % totalFlexFactor
for index := range cols {
if !cols[index].isFlex() {
continue
}
width := flexUnit * (cols[index].flexFactor / flexGCD)
if leftoverWidth > 0 {
width++
leftoverWidth--
}
if index == len(cols)-1 {
width += leftoverWidth
leftoverWidth = 0
}
width = max(width, 1)
cols[index].width = width
// Take borders into account for the actual style
cols[index].style = cols[index].style.Width(width)
}
}
func (m *Model) recalculateHeight() {
header := m.renderHeaders()
headerHeight := 1 // Header always has the top border
if m.headerVisible {
headerHeight = lipgloss.Height(header)
}
footer := m.renderFooter(lipgloss.Width(header), false)
var footerHeight int
if footer != "" {
footerHeight = lipgloss.Height(footer)
}
m.metaHeight = headerHeight + footerHeight
}
func (m *Model) calculatePadding(numRows int) int {
if m.minimumHeight == 0 {
return 0
}
padding := m.minimumHeight - m.metaHeight - numRows - 1 // additional 1 for bottom border
if padding == 0 && numRows == 0 {
// This is an edge case where we want to add 1 additional line of height, i.e.
// add a border without an empty row. However, this is not possible, so we need
// to add an extra row which will result in the table being 1 row taller than
// the requested minimum height.
return 1
}
if padding < 0 {
// Table is already larger than minimum height, do nothing.
return 0
}
return padding
}

39
vendor/github.com/evertras/bubble-table/table/doc.go generated vendored Normal file
View File

@ -0,0 +1,39 @@
/*
Package table contains a Bubble Tea component for an interactive and customizable
table.
The simplest useful table can be created with table.New(...).WithRows(...). Row
data should map to the column keys, as shown below. Note that extra data will
simply not be shown, while missing data will be safely blank in the row's cell.
const (
// This is not necessary, but recommended to avoid typos
columnKeyName = "name"
columnKeyCount = "count"
)
// Define the columns and how they appear
columns := []table.Column{
table.NewColumn(columnKeyName, "Name", 10),
table.NewColumn(columnKeyCount, "Count", 6),
}
// Define the data that will be in the table, mapping to the column keys
rows := []table.Row{
table.NewRow(table.RowData{
columnKeyName: "Cheeseburger",
columnKeyCount: 3,
}),
table.NewRow(table.RowData{
columnKeyName: "Fries",
columnKeyCount: 2,
}),
}
// Create the table
tbl := table.New(columns).WithRows(rows)
// Use it like any Bubble Tea component in your view
tbl.View()
*/
package table

View File

@ -0,0 +1,60 @@
package table
// UserEvent is some state change that has occurred due to user input. These will
// ONLY be generated when a user has interacted directly with the table. These
// will NOT be generated when code programmatically changes values in the table.
type UserEvent any
func (m *Model) appendUserEvent(e UserEvent) {
m.lastUpdateUserEvents = append(m.lastUpdateUserEvents, e)
}
func (m *Model) clearUserEvents() {
m.lastUpdateUserEvents = nil
}
// GetLastUpdateUserEvents returns a list of events that happened due to user
// input in the last Update call. This is useful to look for triggers such as
// whether the user moved to a new highlighted row.
func (m *Model) GetLastUpdateUserEvents() []UserEvent {
// Most common case
if len(m.lastUpdateUserEvents) == 0 {
return nil
}
returned := make([]UserEvent, len(m.lastUpdateUserEvents))
// Slightly wasteful but helps guarantee immutability, and this should only
// have data very rarely so this is fine
copy(returned, m.lastUpdateUserEvents)
return returned
}
// UserEventHighlightedIndexChanged indicates that the user has scrolled to a new
// row.
type UserEventHighlightedIndexChanged struct {
// PreviousRow is the row that was selected before the change.
PreviousRowIndex int
// SelectedRow is the row index that is now selected
SelectedRowIndex int
}
// UserEventRowSelectToggled indicates that the user has either selected or
// deselected a row by toggling the selection. The event contains information
// about which row index was selected and whether it was selected or deselected.
type UserEventRowSelectToggled struct {
RowIndex int
IsSelected bool
}
// UserEventFilterInputFocused indicates that the user has focused the filter
// text input, so that any other typing will type into the filter field. Only
// activates for the built-in filter text box.
type UserEventFilterInputFocused struct{}
// UserEventFilterInputUnfocused indicates that the user has unfocused the filter
// text input, which means the user is done typing into the filter field. Only
// activates for the built-in filter text box.
type UserEventFilterInputUnfocused struct{}

164
vendor/github.com/evertras/bubble-table/table/filter.go generated vendored Normal file
View File

@ -0,0 +1,164 @@
package table
import (
"fmt"
"strings"
)
// FilterFuncInput is the input to a FilterFunc. It's a struct so we can add more things later
// without breaking compatibility.
type FilterFuncInput struct {
// Columns is a list of the columns of the table
Columns []Column
// Row is the row that's being considered for filtering
Row Row
// GlobalMetadata is an arbitrary set of metadata from the table set by WithGlobalMetadata
GlobalMetadata map[string]any
// Filter is the filter string input to consider
Filter string
}
// FilterFunc takes a FilterFuncInput and returns true if the row should be visible,
// or false if the row should be hidden.
type FilterFunc func(FilterFuncInput) bool
func (m Model) getFilteredRows(rows []Row) []Row {
filterInputValue := m.filterTextInput.Value()
if !m.filtered || filterInputValue == "" {
return rows
}
filteredRows := make([]Row, 0)
for _, row := range rows {
var availableFilterFunc FilterFunc
if m.filterFunc != nil {
availableFilterFunc = m.filterFunc
} else {
availableFilterFunc = filterFuncContains
}
if availableFilterFunc(FilterFuncInput{
Columns: m.columns,
Row: row,
Filter: filterInputValue,
GlobalMetadata: m.metadata,
}) {
filteredRows = append(filteredRows, row)
}
}
return filteredRows
}
// filterFuncContains returns a filterFunc that performs case-insensitive
// "contains" matching over all filterable columns in a row.
func filterFuncContains(input FilterFuncInput) bool {
if input.Filter == "" {
return true
}
checkedAny := false
filterLower := strings.ToLower(input.Filter)
for _, column := range input.Columns {
if !column.filterable {
continue
}
checkedAny = true
data, ok := input.Row.Data[column.key]
if !ok {
continue
}
// Extract internal StyledCell data
switch dataV := data.(type) {
case StyledCell:
data = dataV.Data
}
var target string
switch dataV := data.(type) {
case string:
target = dataV
case fmt.Stringer:
target = dataV.String()
default:
target = fmt.Sprintf("%v", data)
}
if strings.Contains(strings.ToLower(target), filterLower) {
return true
}
}
return !checkedAny
}
// filterFuncFuzzy returns a filterFunc that performs case-insensitive fuzzy
// matching (subsequence) over the concatenation of all filterable column values.
func filterFuncFuzzy(input FilterFuncInput) bool {
filter := strings.TrimSpace(input.Filter)
if filter == "" {
return true
}
var builder strings.Builder
for _, col := range input.Columns {
if !col.filterable {
continue
}
value, ok := input.Row.Data[col.key]
if !ok {
continue
}
if sc, ok := value.(StyledCell); ok {
value = sc.Data
}
builder.WriteString(fmt.Sprint(value)) // uses Stringer if implemented
builder.WriteByte(' ')
}
haystack := strings.ToLower(builder.String())
if haystack == "" {
return false
}
for _, token := range strings.Fields(strings.ToLower(filter)) {
if !fuzzySubsequenceMatch(haystack, token) {
return false
}
}
return true
}
// fuzzySubsequenceMatch returns true if all runes in needle appear in order
// within haystack (not necessarily contiguously). Case must be normalized by caller.
func fuzzySubsequenceMatch(haystack, needle string) bool {
if needle == "" {
return true
}
haystackIndex, needleIndex := 0, 0
haystackRunes := []rune(haystack)
needleRunes := []rune(needle)
for haystackIndex < len(haystackRunes) && needleIndex < len(needleRunes) {
if haystackRunes[haystackIndex] == needleRunes[needleIndex] {
needleIndex++
}
haystackIndex++
}
return needleIndex == len(needleRunes)
}

View File

@ -0,0 +1,51 @@
package table
import (
"fmt"
"strings"
)
func (m Model) hasFooter() bool {
return m.footerVisible && (m.staticFooter != "" || m.pageSize != 0 || m.filtered)
}
func (m Model) renderFooter(width int, includeTop bool) string {
if !m.hasFooter() {
return ""
}
const borderAdjustment = 2
styleFooter := m.baseStyle.Copy().Inherit(m.border.styleFooter).Width(width - borderAdjustment)
if includeTop {
styleFooter = styleFooter.BorderTop(true)
}
if m.staticFooter != "" {
return styleFooter.Render(m.staticFooter)
}
sections := []string{}
if m.filtered && (m.filterTextInput.Focused() || m.filterTextInput.Value() != "") {
sections = append(sections, m.filterTextInput.View())
}
// paged feature enabled
if m.pageSize != 0 {
str := fmt.Sprintf("%d/%d", m.CurrentPage(), m.MaxPages())
if m.filtered && m.filterTextInput.Focused() {
// Need to apply inline style here in case of filter input cursor, because
// the input cursor resets the style after rendering. Note that Inline(true)
// creates a copy, so it's safe to use here without mutating the underlying
// base style.
str = m.baseStyle.Inline(true).Render(str)
}
sections = append(sections, str)
}
footerText := strings.Join(sections, " ")
return styleFooter.Render(footerText)
}

View File

@ -0,0 +1,93 @@
package table
import "github.com/charmbracelet/lipgloss"
// This is long and could use some refactoring in the future, but unsure of how
// to pick it apart right now.
//
//nolint:funlen,cyclop
func (m Model) renderHeaders() string {
headerStrings := []string{}
totalRenderedWidth := 0
headerStyles := m.styleHeaders()
renderHeader := func(column Column, borderStyle lipgloss.Style) string {
borderStyle = borderStyle.Inherit(column.style).Inherit(m.baseStyle)
headerSection := limitStr(column.title, column.width)
return borderStyle.Render(headerSection)
}
for columnIndex, column := range m.columns {
var borderStyle lipgloss.Style
if m.horizontalScrollOffsetCol > 0 && columnIndex == m.horizontalScrollFreezeColumnsCount {
if columnIndex == 0 {
borderStyle = headerStyles.left.Copy()
} else {
borderStyle = headerStyles.inner.Copy()
}
rendered := renderHeader(genOverflowColumnLeft(1), borderStyle)
totalRenderedWidth += lipgloss.Width(rendered)
headerStrings = append(headerStrings, rendered)
}
if columnIndex >= m.horizontalScrollFreezeColumnsCount &&
columnIndex < m.horizontalScrollOffsetCol+m.horizontalScrollFreezeColumnsCount {
continue
}
if len(headerStrings) == 0 {
borderStyle = headerStyles.left.Copy()
} else if columnIndex < len(m.columns)-1 {
borderStyle = headerStyles.inner.Copy()
} else {
borderStyle = headerStyles.right.Copy()
}
rendered := renderHeader(column, borderStyle)
if m.maxTotalWidth != 0 {
renderedWidth := lipgloss.Width(rendered)
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(headerStyles.right, overflowWidth)
overflowColumn := genOverflowColumnRight(overflowWidth)
overflowStr := renderHeader(overflowColumn, overflowStyle)
headerStrings = append(headerStrings, overflowStr)
break
}
totalRenderedWidth += renderedWidth
}
headerStrings = append(headerStrings, rendered)
}
headerBlock := lipgloss.JoinHorizontal(lipgloss.Bottom, headerStrings...)
return headerBlock
}

120
vendor/github.com/evertras/bubble-table/table/keys.go generated vendored Normal file
View File

@ -0,0 +1,120 @@
package table
import "github.com/charmbracelet/bubbles/key"
// KeyMap defines the keybindings for the table when it's focused.
type KeyMap struct {
RowDown key.Binding
RowUp key.Binding
RowSelectToggle key.Binding
PageDown key.Binding
PageUp key.Binding
PageFirst key.Binding
PageLast key.Binding
// Filter allows the user to start typing and filter the rows.
Filter key.Binding
// FilterBlur is the key that stops the user's input from typing into the filter.
FilterBlur key.Binding
// FilterClear will clear the filter while it's blurred.
FilterClear key.Binding
// ScrollRight will move one column to the right when overflow occurs.
ScrollRight key.Binding
// ScrollLeft will move one column to the left when overflow occurs.
ScrollLeft key.Binding
}
// DefaultKeyMap returns a set of sensible defaults for controlling a focused table with help text.
func DefaultKeyMap() KeyMap {
return KeyMap{
RowDown: key.NewBinding(
key.WithKeys("down", "j"),
key.WithHelp("↓/j", "move down"),
),
RowUp: key.NewBinding(
key.WithKeys("up", "k"),
key.WithHelp("↑/k", "move up"),
),
RowSelectToggle: key.NewBinding(
key.WithKeys(" ", "enter"),
key.WithHelp("<space>/enter", "select row"),
),
PageDown: key.NewBinding(
key.WithKeys("right", "l", "pgdown"),
key.WithHelp("→/h/page down", "next page"),
),
PageUp: key.NewBinding(
key.WithKeys("left", "h", "pgup"),
key.WithHelp("←/h/page up", "previous page"),
),
PageFirst: key.NewBinding(
key.WithKeys("home", "g"),
key.WithHelp("home/g", "first page"),
),
PageLast: key.NewBinding(
key.WithKeys("end", "G"),
key.WithHelp("end/G", "last page"),
),
Filter: key.NewBinding(
key.WithKeys("/"),
key.WithHelp("/", "filter"),
),
FilterBlur: key.NewBinding(
key.WithKeys("enter", "esc"),
key.WithHelp("enter/esc", "unfocus"),
),
FilterClear: key.NewBinding(
key.WithKeys("esc"),
key.WithHelp("esc", "clear filter"),
),
ScrollRight: key.NewBinding(
key.WithKeys("shift+right"),
key.WithHelp("shift+→", "scroll right"),
),
ScrollLeft: key.NewBinding(
key.WithKeys("shift+left"),
key.WithHelp("shift+←", "scroll left"),
),
}
}
// FullHelp returns a multi row view of all the helpkeys that are defined. Needed to fullfil the 'help.Model' interface.
// Also appends all user defined extra keys to the help.
func (m Model) FullHelp() [][]key.Binding {
keyBinds := [][]key.Binding{
{m.keyMap.RowDown, m.keyMap.RowUp, m.keyMap.RowSelectToggle},
{m.keyMap.PageDown, m.keyMap.PageUp, m.keyMap.PageFirst, m.keyMap.PageLast},
{m.keyMap.Filter, m.keyMap.FilterBlur, m.keyMap.FilterClear, m.keyMap.ScrollRight, m.keyMap.ScrollLeft},
}
if m.additionalFullHelpKeys != nil {
keyBinds = append(keyBinds, m.additionalFullHelpKeys())
}
return keyBinds
}
// ShortHelp just returns a single row of help views. Needed to fullfil the 'help.Model' interface.
// Also appends all user defined extra keys to the help.
func (m Model) ShortHelp() []key.Binding {
keyBinds := []key.Binding{
m.keyMap.RowDown,
m.keyMap.RowUp,
m.keyMap.RowSelectToggle,
m.keyMap.PageDown,
m.keyMap.PageUp,
m.keyMap.Filter,
m.keyMap.FilterBlur,
m.keyMap.FilterClear,
}
if m.additionalShortHelpKeys != nil {
keyBinds = append(keyBinds, m.additionalShortHelpKeys()...)
}
return keyBinds
}

148
vendor/github.com/evertras/bubble-table/table/model.go generated vendored Normal file
View File

@ -0,0 +1,148 @@
package table
import (
"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
const (
columnKeySelect = "___select___"
)
var (
defaultHighlightStyle = lipgloss.NewStyle().Background(lipgloss.Color("#334"))
)
// Model is the main table model. Create using New().
type Model struct {
// Data
columns []Column
rows []Row
metadata map[string]any
// Caches for optimizations
visibleRowCacheUpdated bool
visibleRowCache []Row
// Shown when data is missing from a row
missingDataIndicator any
// Interaction
focused bool
keyMap KeyMap
// Taken from: 'Bubbles/List'
// Additional key mappings for the short and full help views. This allows
// you to add additional key mappings to the help menu without
// re-implementing the help component. Of course, you can also disable the
// list's help component and implement a new one if you need more
// flexibility.
// You have to supply a keybinding like this:
// key.NewBinding( key.WithKeys("shift+left"), key.WithHelp("shift+←", "scroll left"))
// It needs both 'WithKeys' and 'WithHelp'
additionalShortHelpKeys func() []key.Binding
additionalFullHelpKeys func() []key.Binding
selectableRows bool
rowCursorIndex int
// Events
lastUpdateUserEvents []UserEvent
// Styles
baseStyle lipgloss.Style
highlightStyle lipgloss.Style
headerStyle lipgloss.Style
rowStyleFunc func(RowStyleFuncInput) lipgloss.Style
border Border
selectedText string
unselectedText string
// Header
headerVisible bool
// Footers
footerVisible bool
staticFooter string
// Pagination
pageSize int
currentPage int
paginationWrapping bool
// Sorting, where a stable sort is applied from first element to last so
// that elements are grouped by the later elements.
sortOrder []SortColumn
// Filter
filtered bool
filterTextInput textinput.Model
filterFunc FilterFunc
// For flex columns
targetTotalWidth int
// The maximum total width for overflow/scrolling
maxTotalWidth int
// Internal cached calculations for reference, may be higher than
// maxTotalWidth. If this is the case, we need to adjust the view
totalWidth int
// How far to scroll to the right, in columns
horizontalScrollOffsetCol int
// How many columns to freeze when scrolling horizontally
horizontalScrollFreezeColumnsCount int
// Calculated maximum column we can scroll to before the last is displayed
maxHorizontalColumnIndex int
// Minimum total height of the table
minimumHeight int
// Internal cached calculation, the height of the header and footer
// including borders. Used to determine how many padding rows to add.
metaHeight int
// If true, the table will be multiline
multiline bool
}
// New creates a new table ready for further modifications.
func New(columns []Column) Model {
filterInput := textinput.New()
filterInput.Prompt = "/"
model := Model{
columns: make([]Column, len(columns)),
metadata: make(map[string]any),
highlightStyle: defaultHighlightStyle.Copy(),
border: borderDefault,
headerVisible: true,
footerVisible: true,
keyMap: DefaultKeyMap(),
selectedText: "[x]",
unselectedText: "[ ]",
filterTextInput: filterInput,
filterFunc: filterFuncContains,
baseStyle: lipgloss.NewStyle().Align(lipgloss.Right),
paginationWrapping: true,
}
// Do a full deep copy to avoid unexpected edits
copy(model.columns, columns)
model.recalculateWidth()
return model
}
// Init initializes the table per the Bubble Tea architecture.
func (m Model) Init() tea.Cmd {
return nil
}

View File

@ -0,0 +1,510 @@
package table
import (
"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/textinput"
"github.com/charmbracelet/lipgloss"
)
// RowStyleFuncInput is the input to the style function that can
// be applied to each row. This is useful for things like zebra
// striping or other data-based styles.
//
// Note that we use a struct here to allow for future expansion
// while keeping backwards compatibility.
type RowStyleFuncInput struct {
// Index is the index of the row, starting at 0.
Index int
// Row is the full row data.
Row Row
// IsHighlighted is true if the row is currently highlighted.
IsHighlighted bool
}
// WithRowStyleFunc sets a function that can be used to apply a style to each row
// based on the row data. This is useful for things like zebra striping or other
// data-based styles. It can be safely set to nil to remove it later.
// This style is applied after the base style and before individual row styles.
// This will override any HighlightStyle settings.
func (m Model) WithRowStyleFunc(f func(RowStyleFuncInput) lipgloss.Style) Model {
m.rowStyleFunc = f
return m
}
// WithHighlightedRow sets the highlighted row to the given index.
func (m Model) WithHighlightedRow(index int) Model {
m.rowCursorIndex = index
if m.rowCursorIndex >= len(m.GetVisibleRows()) {
m.rowCursorIndex = len(m.GetVisibleRows()) - 1
}
if m.rowCursorIndex < 0 {
m.rowCursorIndex = 0
}
m.currentPage = m.expectedPageForRowIndex(m.rowCursorIndex)
return m
}
// HeaderStyle sets the style to apply to the header text, such as color or bold.
func (m Model) HeaderStyle(style lipgloss.Style) Model {
m.headerStyle = style.Copy()
return m
}
// WithRows sets the rows to show as data in the table.
func (m Model) WithRows(rows []Row) Model {
m.rows = rows
m.visibleRowCacheUpdated = false
if m.rowCursorIndex >= len(m.rows) {
m.rowCursorIndex = len(m.rows) - 1
}
if m.rowCursorIndex < 0 {
m.rowCursorIndex = 0
}
if m.pageSize != 0 {
maxPage := m.MaxPages()
// MaxPages is 1-index, currentPage is 0 index
if maxPage <= m.currentPage {
m.pageLast()
}
}
return m
}
// WithKeyMap sets the key map to use for controls when focused.
func (m Model) WithKeyMap(keyMap KeyMap) Model {
m.keyMap = keyMap
return m
}
// KeyMap returns a copy of the current key map in use.
func (m Model) KeyMap() KeyMap {
return m.keyMap
}
// SelectableRows sets whether or not rows are selectable. If set, adds a column
// in the front that acts as a checkbox and responds to controls if Focused.
func (m Model) SelectableRows(selectable bool) Model {
m.selectableRows = selectable
hasSelectColumn := len(m.columns) > 0 && m.columns[0].key == columnKeySelect
if hasSelectColumn != selectable {
if selectable {
m.columns = append([]Column{
NewColumn(columnKeySelect, m.selectedText, len([]rune(m.selectedText))),
}, m.columns...)
} else {
m.columns = m.columns[1:]
}
}
m.recalculateWidth()
return m
}
// HighlightedRow returns the full Row that's currently highlighted by the user.
func (m Model) HighlightedRow() Row {
if len(m.GetVisibleRows()) > 0 {
return m.GetVisibleRows()[m.rowCursorIndex]
}
// TODO: Better way to do this without pointers/nil? Or should it be nil?
return Row{}
}
// SelectedRows returns all rows that have been set as selected by the user.
func (m Model) SelectedRows() []Row {
selectedRows := []Row{}
for _, row := range m.GetVisibleRows() {
if row.selected {
selectedRows = append(selectedRows, row)
}
}
return selectedRows
}
// HighlightStyle sets a custom style to use when the row is being highlighted
// by the cursor. This should not be used with WithRowStyleFunc. Instead, use
// the IsHighlighted field in the style function.
func (m Model) HighlightStyle(style lipgloss.Style) Model {
m.highlightStyle = style
return m
}
// Focused allows the table to show highlighted rows and take in controls of
// up/down/space/etc to let the user navigate the table and interact with it.
func (m Model) Focused(focused bool) Model {
m.focused = focused
return m
}
// Filtered allows the table to show rows that match the filter.
func (m Model) Filtered(filtered bool) Model {
m.filtered = filtered
m.visibleRowCacheUpdated = false
if m.minimumHeight > 0 {
m.recalculateHeight()
}
return m
}
// StartFilterTyping focuses the text input to allow user typing to filter.
func (m Model) StartFilterTyping() Model {
m.filterTextInput.Focus()
return m
}
// WithStaticFooter adds a footer that only displays the given text.
func (m Model) WithStaticFooter(footer string) Model {
m.staticFooter = footer
if m.minimumHeight > 0 {
m.recalculateHeight()
}
return m
}
// WithPageSize enables pagination using the given page size. This can be called
// again at any point to resize the height of the table.
func (m Model) WithPageSize(pageSize int) Model {
m.pageSize = pageSize
maxPages := m.MaxPages()
if m.currentPage >= maxPages {
m.currentPage = maxPages - 1
}
if m.minimumHeight > 0 {
m.recalculateHeight()
}
return m
}
// WithNoPagination disables pagination in the table.
func (m Model) WithNoPagination() Model {
m.pageSize = 0
if m.minimumHeight > 0 {
m.recalculateHeight()
}
return m
}
// WithPaginationWrapping sets whether to wrap around from the beginning to the
// end when navigating through pages. Defaults to true.
func (m Model) WithPaginationWrapping(wrapping bool) Model {
m.paginationWrapping = wrapping
return m
}
// WithSelectedText describes what text to show when selectable rows are enabled.
// The selectable column header will use the selected text string.
func (m Model) WithSelectedText(unselected, selected string) Model {
m.selectedText = selected
m.unselectedText = unselected
if len(m.columns) > 0 && m.columns[0].key == columnKeySelect {
m.columns[0] = NewColumn(columnKeySelect, m.selectedText, len([]rune(m.selectedText)))
m.recalculateWidth()
}
return m
}
// WithBaseStyle applies a base style as the default for everything in the table.
// This is useful for border colors, default alignment, default color, etc.
func (m Model) WithBaseStyle(style lipgloss.Style) Model {
m.baseStyle = style
return m
}
// WithTargetWidth sets the total target width of the table, including borders.
// This only takes effect when using flex columns. When using flex columns,
// columns will stretch to fill out to the total width given here.
func (m Model) WithTargetWidth(totalWidth int) Model {
m.targetTotalWidth = totalWidth
m.recalculateWidth()
return m
}
// WithMinimumHeight sets the minimum total height of the table, including borders.
func (m Model) WithMinimumHeight(minimumHeight int) Model {
m.minimumHeight = minimumHeight
m.recalculateHeight()
return m
}
// PageDown goes to the next page of a paginated table, wrapping to the first
// page if the table is already on the last page.
func (m Model) PageDown() Model {
m.pageDown()
return m
}
// PageUp goes to the previous page of a paginated table, wrapping to the
// last page if the table is already on the first page.
func (m Model) PageUp() Model {
m.pageUp()
return m
}
// PageLast goes to the last page of a paginated table.
func (m Model) PageLast() Model {
m.pageLast()
return m
}
// PageFirst goes to the first page of a paginated table.
func (m Model) PageFirst() Model {
m.pageFirst()
return m
}
// WithCurrentPage sets the current page (1 as the first page) of a paginated
// table, bounded to the total number of pages. The current selected row will
// be set to the top row of the page if the page changed.
func (m Model) WithCurrentPage(currentPage int) Model {
if m.pageSize == 0 || currentPage == m.CurrentPage() {
return m
}
if currentPage < 1 {
currentPage = 1
} else {
maxPages := m.MaxPages()
if currentPage > maxPages {
currentPage = maxPages
}
}
m.currentPage = currentPage - 1
m.rowCursorIndex = m.currentPage * m.pageSize
return m
}
// WithColumns sets the visible columns for the table, so that columns can be
// added/removed/resized or headers rewritten.
func (m Model) WithColumns(columns []Column) Model {
// Deep copy to avoid edits
m.columns = make([]Column, len(columns))
copy(m.columns, columns)
m.recalculateWidth()
if m.selectableRows {
// Re-add the selectable column
m = m.SelectableRows(true)
}
return m
}
// WithFilterInput makes the table use the provided text input bubble for
// filtering rather than using the built-in default. This allows for external
// text input controls to be used.
func (m Model) WithFilterInput(input textinput.Model) Model {
if m.filterTextInput.Value() != input.Value() {
m.pageFirst()
}
m.filterTextInput = input
m.visibleRowCacheUpdated = false
return m
}
// WithFilterInputValue sets the filter value to the given string, immediately
// applying it as if the user had typed it in. Useful for external filter inputs
// that are not necessarily a text input.
func (m Model) WithFilterInputValue(value string) Model {
if m.filterTextInput.Value() != value {
m.pageFirst()
}
m.filterTextInput.SetValue(value)
m.filterTextInput.Blur()
m.visibleRowCacheUpdated = false
return m
}
// WithFilterFunc adds a filter function to the model. If the function returns
// true, the row will be included in the filtered results. If the function
// is nil, the function won't be used and instead the default filtering will be applied,
// if any.
func (m Model) WithFilterFunc(shouldInclude FilterFunc) Model {
m.filterFunc = shouldInclude
m.visibleRowCacheUpdated = false
return m
}
// WithFuzzyFilter enables fuzzy filtering for the table.
func (m Model) WithFuzzyFilter() Model {
return m.WithFilterFunc(filterFuncFuzzy)
}
// WithFooterVisibility sets the visibility of the footer.
func (m Model) WithFooterVisibility(visibility bool) Model {
m.footerVisible = visibility
if m.minimumHeight > 0 {
m.recalculateHeight()
}
return m
}
// WithHeaderVisibility sets the visibility of the header.
func (m Model) WithHeaderVisibility(visibility bool) Model {
m.headerVisible = visibility
if m.minimumHeight > 0 {
m.recalculateHeight()
}
return m
}
// WithMaxTotalWidth sets the maximum total width that the table should render.
// If this width is exceeded by either the target width or by the total width
// of all the columns (including borders!), anything extra will be treated as
// overflow and horizontal scrolling will be enabled to see the rest.
func (m Model) WithMaxTotalWidth(maxTotalWidth int) Model {
m.maxTotalWidth = maxTotalWidth
m.recalculateWidth()
return m
}
// WithHorizontalFreezeColumnCount freezes the given number of columns to the
// left side. This is useful for things like ID or Name columns that should
// always be visible even when scrolling.
func (m Model) WithHorizontalFreezeColumnCount(columnsToFreeze int) Model {
m.horizontalScrollFreezeColumnsCount = columnsToFreeze
m.recalculateWidth()
return m
}
// ScrollRight moves one column to the right. Use with WithMaxTotalWidth.
func (m Model) ScrollRight() Model {
m.scrollRight()
return m
}
// ScrollLeft moves one column to the left. Use with WithMaxTotalWidth.
func (m Model) ScrollLeft() Model {
m.scrollLeft()
return m
}
// WithMissingDataIndicator sets an indicator to use when data for a column is
// not found in a given row. Note that this is for completely missing data,
// an empty string or other zero value that is explicitly set is not considered
// to be missing.
func (m Model) WithMissingDataIndicator(str string) Model {
m.missingDataIndicator = str
return m
}
// WithMissingDataIndicatorStyled sets a styled indicator to use when data for
// a column is not found in a given row. Note that this is for completely
// missing data, an empty string or other zero value that is explicitly set is
// not considered to be missing.
func (m Model) WithMissingDataIndicatorStyled(styled StyledCell) Model {
m.missingDataIndicator = styled
return m
}
// WithAllRowsDeselected deselects any rows that are currently selected.
func (m Model) WithAllRowsDeselected() Model {
rows := m.GetVisibleRows()
for i, row := range rows {
if row.selected {
rows[i] = row.Selected(false)
}
}
m.rows = rows
return m
}
// WithMultiline sets whether or not to wrap text in cells to multiple lines.
func (m Model) WithMultiline(multiline bool) Model {
m.multiline = multiline
return m
}
// WithAdditionalShortHelpKeys enables you to add more keybindings to the 'short help' view.
func (m Model) WithAdditionalShortHelpKeys(keys []key.Binding) Model {
m.additionalShortHelpKeys = func() []key.Binding {
return keys
}
return m
}
// WithAdditionalFullHelpKeys enables you to add more keybindings to the 'full help' view.
func (m Model) WithAdditionalFullHelpKeys(keys []key.Binding) Model {
m.additionalFullHelpKeys = func() []key.Binding {
return keys
}
return m
}
// WithGlobalMetadata applies the given metadata to the table. This metadata is passed to
// some functions in FilterFuncInput and StyleFuncInput to enable more advanced decisions,
// such as setting some global theme variable to reference, etc. Has no effect otherwise.
func (m Model) WithGlobalMetadata(metadata map[string]any) Model {
m.metadata = metadata
return m
}

View File

@ -0,0 +1,18 @@
package table
import "github.com/charmbracelet/lipgloss"
const columnKeyOverflowRight = "___overflow_r___"
const columnKeyOverflowLeft = "___overflow_l__"
func genOverflowStyle(base lipgloss.Style, width int) lipgloss.Style {
return base.Width(width).Align(lipgloss.Right)
}
func genOverflowColumnRight(width int) Column {
return NewColumn(columnKeyOverflowRight, ">", width)
}
func genOverflowColumnLeft(width int) Column {
return NewColumn(columnKeyOverflowLeft, "<", width)
}

View File

@ -0,0 +1,112 @@
package table
// PageSize returns the current page size for the table, or 0 if there is no
// pagination enabled.
func (m *Model) PageSize() int {
return m.pageSize
}
// CurrentPage returns the current page that the table is on, starting from an
// index of 1.
func (m *Model) CurrentPage() int {
return m.currentPage + 1
}
// MaxPages returns the maximum number of pages that are visible.
func (m *Model) MaxPages() int {
totalRows := len(m.GetVisibleRows())
if m.pageSize == 0 || totalRows == 0 {
return 1
}
return (totalRows-1)/m.pageSize + 1
}
// TotalRows returns the current total row count of the table. If the table is
// paginated, this is the total number of rows across all pages.
func (m *Model) TotalRows() int {
return len(m.GetVisibleRows())
}
// VisibleIndices returns the current visible rows by their 0 based index.
// Useful for custom pagination footers.
func (m *Model) VisibleIndices() (start, end int) {
totalRows := len(m.GetVisibleRows())
if m.pageSize == 0 {
start = 0
end = totalRows - 1
return start, end
}
start = m.pageSize * m.currentPage
end = start + m.pageSize - 1
if end >= totalRows {
end = totalRows - 1
}
return start, end
}
func (m *Model) pageDown() {
if m.pageSize == 0 || len(m.GetVisibleRows()) <= m.pageSize {
return
}
m.currentPage++
maxPageIndex := m.MaxPages() - 1
if m.currentPage > maxPageIndex {
if m.paginationWrapping {
m.currentPage = 0
} else {
m.currentPage = maxPageIndex
}
}
m.rowCursorIndex = m.currentPage * m.pageSize
}
func (m *Model) pageUp() {
if m.pageSize == 0 || len(m.GetVisibleRows()) <= m.pageSize {
return
}
m.currentPage--
maxPageIndex := m.MaxPages() - 1
if m.currentPage < 0 {
if m.paginationWrapping {
m.currentPage = maxPageIndex
} else {
m.currentPage = 0
}
}
m.rowCursorIndex = m.currentPage * m.pageSize
}
func (m *Model) pageFirst() {
m.currentPage = 0
m.rowCursorIndex = 0
}
func (m *Model) pageLast() {
m.currentPage = m.MaxPages() - 1
m.rowCursorIndex = m.currentPage * m.pageSize
}
func (m *Model) expectedPageForRowIndex(rowIndex int) int {
if m.pageSize == 0 {
return 0
}
expectedPage := rowIndex / m.pageSize
return expectedPage
}

96
vendor/github.com/evertras/bubble-table/table/query.go generated vendored Normal file
View File

@ -0,0 +1,96 @@
package table
// GetColumnSorting returns the current sorting rules for the table as a list of
// SortColumns, which are applied from first to last. This means that data will
// be grouped by the later elements in the list. The returned list is a copy
// and modifications will have no effect.
func (m *Model) GetColumnSorting() []SortColumn {
c := make([]SortColumn, len(m.sortOrder))
copy(c, m.sortOrder)
return c
}
// GetCanFilter returns true if the table enables filtering at all. This does
// not say whether a filter is currently active, only that the feature is enabled.
func (m *Model) GetCanFilter() bool {
return m.filtered
}
// GetIsFilterActive returns true if the table is currently being filtered. This
// does not say whether the table CAN be filtered, only whether or not a filter
// is actually currently being applied.
func (m *Model) GetIsFilterActive() bool {
return m.filterTextInput.Value() != ""
}
// GetIsFilterInputFocused returns true if the table's built-in filter input is
// currently focused.
func (m *Model) GetIsFilterInputFocused() bool {
return m.filterTextInput.Focused()
}
// GetCurrentFilter returns the current filter text being applied, or an empty
// string if none is applied.
func (m *Model) GetCurrentFilter() string {
return m.filterTextInput.Value()
}
// GetVisibleRows returns sorted and filtered rows.
func (m *Model) GetVisibleRows() []Row {
if m.visibleRowCacheUpdated {
return m.visibleRowCache
}
rows := make([]Row, len(m.rows))
copy(rows, m.rows)
if m.filtered {
rows = m.getFilteredRows(rows)
}
rows = getSortedRows(m.sortOrder, rows)
m.visibleRowCache = rows
m.visibleRowCacheUpdated = true
return rows
}
// GetHighlightedRowIndex returns the index of the Row that's currently highlighted
// by the user.
func (m *Model) GetHighlightedRowIndex() int {
return m.rowCursorIndex
}
// GetFocused returns whether or not the table is focused and is receiving inputs.
func (m *Model) GetFocused() bool {
return m.focused
}
// GetHorizontalScrollColumnOffset returns how many columns to the right the table
// has been scrolled. 0 means the table is all the way to the left, which is
// the starting default.
func (m *Model) GetHorizontalScrollColumnOffset() int {
return m.horizontalScrollOffsetCol
}
// GetHeaderVisibility returns true if the header has been set to visible (default)
// or false if the header has been set to hidden.
func (m *Model) GetHeaderVisibility() bool {
return m.headerVisible
}
// GetFooterVisibility returns true if the footer has been set to
// visible (default) or false if the footer has been set to hidden.
// Note that even if the footer is visible it will only be rendered if
// it has contents.
func (m *Model) GetFooterVisibility() bool {
return m.footerVisible
}
// GetPaginationWrapping returns true if pagination wrapping is enabled, or false
// if disabled. If disabled, navigating through pages will stop at the first
// and last pages.
func (m *Model) GetPaginationWrapping() bool {
return m.paginationWrapping
}

252
vendor/github.com/evertras/bubble-table/table/row.go generated vendored Normal file
View File

@ -0,0 +1,252 @@
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
}

View File

@ -0,0 +1,50 @@
package table
func (m *Model) scrollRight() {
if m.horizontalScrollOffsetCol < m.maxHorizontalColumnIndex {
m.horizontalScrollOffsetCol++
}
}
func (m *Model) scrollLeft() {
if m.horizontalScrollOffsetCol > 0 {
m.horizontalScrollOffsetCol--
}
}
func (m *Model) recalculateLastHorizontalColumn() {
if m.horizontalScrollFreezeColumnsCount >= len(m.columns) {
m.maxHorizontalColumnIndex = 0
return
}
if m.totalWidth <= m.maxTotalWidth {
m.maxHorizontalColumnIndex = 0
return
}
const (
leftOverflowWidth = 2
borderAdjustment = 1
)
// Always have left border
visibleWidth := borderAdjustment + leftOverflowWidth
for i := 0; i < m.horizontalScrollFreezeColumnsCount; i++ {
visibleWidth += m.columns[i].width + borderAdjustment
}
m.maxHorizontalColumnIndex = len(m.columns) - 1
// Work backwards from the right
for i := len(m.columns) - 1; i >= m.horizontalScrollFreezeColumnsCount && visibleWidth <= m.maxTotalWidth; i-- {
visibleWidth += m.columns[i].width + borderAdjustment
if visibleWidth <= m.maxTotalWidth {
m.maxHorizontalColumnIndex = i - m.horizontalScrollFreezeColumnsCount
}
}
}

178
vendor/github.com/evertras/bubble-table/table/sort.go generated vendored Normal file
View File

@ -0,0 +1,178 @@
package table
import (
"fmt"
"sort"
)
// SortDirection indicates whether a column should sort by ascending or descending.
type SortDirection int
const (
// SortDirectionAsc indicates the column should be in ascending order.
SortDirectionAsc SortDirection = iota
// SortDirectionDesc indicates the column should be in descending order.
SortDirectionDesc
)
// SortColumn describes which column should be sorted and how.
type SortColumn struct {
ColumnKey string
Direction SortDirection
}
// SortByAsc sets the main sorting column to the given key, in ascending order.
// If a previous sort was used, it is replaced by the given column each time
// this function is called. Values are sorted as numbers if possible, or just
// as simple string comparisons if not numbers.
func (m Model) SortByAsc(columnKey string) Model {
m.sortOrder = []SortColumn{
{
ColumnKey: columnKey,
Direction: SortDirectionAsc,
},
}
m.visibleRowCacheUpdated = false
return m
}
// SortByDesc sets the main sorting column to the given key, in descending order.
// If a previous sort was used, it is replaced by the given column each time
// this function is called. Values are sorted as numbers if possible, or just
// as simple string comparisons if not numbers.
func (m Model) SortByDesc(columnKey string) Model {
m.sortOrder = []SortColumn{
{
ColumnKey: columnKey,
Direction: SortDirectionDesc,
},
}
m.visibleRowCacheUpdated = false
return m
}
// ThenSortByAsc provides a secondary sort after the first, in ascending order.
// Can be chained multiple times, applying to smaller subgroups each time.
func (m Model) ThenSortByAsc(columnKey string) Model {
m.sortOrder = append([]SortColumn{
{
ColumnKey: columnKey,
Direction: SortDirectionAsc,
},
}, m.sortOrder...)
m.visibleRowCacheUpdated = false
return m
}
// ThenSortByDesc provides a secondary sort after the first, in descending order.
// Can be chained multiple times, applying to smaller subgroups each time.
func (m Model) ThenSortByDesc(columnKey string) Model {
m.sortOrder = append([]SortColumn{
{
ColumnKey: columnKey,
Direction: SortDirectionDesc,
},
}, m.sortOrder...)
m.visibleRowCacheUpdated = false
return m
}
type sortableTable struct {
rows []Row
byColumn SortColumn
}
func (s *sortableTable) Len() int {
return len(s.rows)
}
func (s *sortableTable) Swap(i, j int) {
old := s.rows[i]
s.rows[i] = s.rows[j]
s.rows[j] = old
}
func (s *sortableTable) extractString(i int, column string) string {
iData, exists := s.rows[i].Data[column]
if !exists {
return ""
}
switch iData := iData.(type) {
case StyledCell:
return fmt.Sprintf("%v", iData.Data)
case string:
return iData
default:
return fmt.Sprintf("%v", iData)
}
}
func (s *sortableTable) extractNumber(i int, column string) (float64, bool) {
iData, exists := s.rows[i].Data[column]
if !exists {
return 0, false
}
return asNumber(iData)
}
func (s *sortableTable) Less(first, second int) bool {
firstNum, firstNumIsValid := s.extractNumber(first, s.byColumn.ColumnKey)
secondNum, secondNumIsValid := s.extractNumber(second, s.byColumn.ColumnKey)
if firstNumIsValid && secondNumIsValid {
if s.byColumn.Direction == SortDirectionAsc {
return firstNum < secondNum
}
return firstNum > secondNum
}
firstVal := s.extractString(first, s.byColumn.ColumnKey)
secondVal := s.extractString(second, s.byColumn.ColumnKey)
if s.byColumn.Direction == SortDirectionAsc {
return firstVal < secondVal
}
return firstVal > secondVal
}
func getSortedRows(sortOrder []SortColumn, rows []Row) []Row {
var sortedRows []Row
if len(sortOrder) == 0 {
sortedRows = rows
return sortedRows
}
sortedRows = make([]Row, len(rows))
copy(sortedRows, rows)
for _, byColumn := range sortOrder {
sorted := &sortableTable{
rows: sortedRows,
byColumn: byColumn,
}
sort.Stable(sorted)
sortedRows = sorted.rows
}
return sortedRows
}

View File

@ -0,0 +1,26 @@
package table
import (
"strings"
"github.com/muesli/reflow/ansi"
"github.com/muesli/reflow/truncate"
)
func limitStr(str string, maxLen int) string {
if maxLen == 0 {
return ""
}
newLineIndex := strings.Index(str, "\n")
if newLineIndex > -1 {
str = str[:newLineIndex] + "…"
}
if ansi.PrintableRuneWidth(str) > maxLen {
// #nosec: G115
return truncate.StringWithTail(str, uint(maxLen), "…")
}
return str
}

154
vendor/github.com/evertras/bubble-table/table/update.go generated vendored Normal file
View File

@ -0,0 +1,154 @@
package table
import (
"github.com/charmbracelet/bubbles/key"
tea "github.com/charmbracelet/bubbletea"
)
func (m *Model) moveHighlightUp() {
m.rowCursorIndex--
if m.rowCursorIndex < 0 {
m.rowCursorIndex = len(m.GetVisibleRows()) - 1
}
m.currentPage = m.expectedPageForRowIndex(m.rowCursorIndex)
}
func (m *Model) moveHighlightDown() {
m.rowCursorIndex++
if m.rowCursorIndex >= len(m.GetVisibleRows()) {
m.rowCursorIndex = 0
}
m.currentPage = m.expectedPageForRowIndex(m.rowCursorIndex)
}
func (m *Model) toggleSelect() {
if !m.selectableRows || len(m.GetVisibleRows()) == 0 {
return
}
rows := m.GetVisibleRows()
rowID := rows[m.rowCursorIndex].id
currentSelectedState := false
for i := range m.rows {
if m.rows[i].id == rowID {
currentSelectedState = m.rows[i].selected
m.rows[i].selected = !m.rows[i].selected
}
}
m.visibleRowCacheUpdated = false
m.appendUserEvent(UserEventRowSelectToggled{
RowIndex: m.rowCursorIndex,
IsSelected: !currentSelectedState,
})
}
func (m Model) updateFilterTextInput(msg tea.Msg) (Model, tea.Cmd) {
var cmd tea.Cmd
switch msg := msg.(type) {
case tea.KeyMsg:
if key.Matches(msg, m.keyMap.FilterBlur) {
m.filterTextInput.Blur()
}
}
m.filterTextInput, cmd = m.filterTextInput.Update(msg)
m.pageFirst()
m.visibleRowCacheUpdated = false
return m, cmd
}
// This is a series of Matches tests with minimal logic
//
//nolint:cyclop
func (m *Model) handleKeypress(msg tea.KeyMsg) {
previousRowIndex := m.rowCursorIndex
if key.Matches(msg, m.keyMap.RowDown) {
m.moveHighlightDown()
}
if key.Matches(msg, m.keyMap.RowUp) {
m.moveHighlightUp()
}
if key.Matches(msg, m.keyMap.RowSelectToggle) {
m.toggleSelect()
}
if key.Matches(msg, m.keyMap.PageDown) {
m.pageDown()
}
if key.Matches(msg, m.keyMap.PageUp) {
m.pageUp()
}
if key.Matches(msg, m.keyMap.PageFirst) {
m.pageFirst()
}
if key.Matches(msg, m.keyMap.PageLast) {
m.pageLast()
}
if key.Matches(msg, m.keyMap.Filter) {
m.filterTextInput.Focus()
m.appendUserEvent(UserEventFilterInputFocused{})
}
if key.Matches(msg, m.keyMap.FilterClear) {
m.visibleRowCacheUpdated = false
m.filterTextInput.Reset()
}
if key.Matches(msg, m.keyMap.ScrollRight) {
m.scrollRight()
}
if key.Matches(msg, m.keyMap.ScrollLeft) {
m.scrollLeft()
}
if m.rowCursorIndex != previousRowIndex {
m.appendUserEvent(UserEventHighlightedIndexChanged{
PreviousRowIndex: previousRowIndex,
SelectedRowIndex: m.rowCursorIndex,
})
}
}
// Update responds to input from the user or other messages from Bubble Tea.
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
m.clearUserEvents()
if !m.focused {
return m, nil
}
if m.filterTextInput.Focused() {
var cmd tea.Cmd
m, cmd = m.updateFilterTextInput(msg)
if !m.filterTextInput.Focused() {
m.appendUserEvent(UserEventFilterInputUnfocused{})
}
return m, cmd
}
switch msg := msg.(type) {
case tea.KeyMsg:
m.handleKeypress(msg)
}
return m, nil
}

65
vendor/github.com/evertras/bubble-table/table/view.go generated vendored Normal file
View File

@ -0,0 +1,65 @@
package table
import (
"strings"
"github.com/charmbracelet/lipgloss"
)
// View renders the table. It does not end in a newline, so that it can be
// composed with other elements more consistently.
//
//nolint:cyclop
func (m Model) View() string {
// Safety valve for empty tables
if len(m.columns) == 0 {
return ""
}
body := strings.Builder{}
rowStrs := make([]string, 0, 1)
headers := m.renderHeaders()
startRowIndex, endRowIndex := m.VisibleIndices()
numRows := endRowIndex - startRowIndex + 1
padding := m.calculatePadding(numRows)
if m.headerVisible {
rowStrs = append(rowStrs, headers)
} else if numRows > 0 || padding > 0 {
//nolint: mnd // This is just getting the first newlined substring
split := strings.SplitN(headers, "\n", 2)
rowStrs = append(rowStrs, split[0])
}
for i := startRowIndex; i <= endRowIndex; i++ {
rowStrs = append(rowStrs, m.renderRow(i, padding == 0 && i == endRowIndex))
}
for i := 1; i <= padding; i++ {
rowStrs = append(rowStrs, m.renderBlankRow(i == padding))
}
var footer string
if len(rowStrs) > 0 {
footer = m.renderFooter(lipgloss.Width(rowStrs[0]), false)
} else {
footer = m.renderFooter(lipgloss.Width(headers), true)
}
if footer != "" {
rowStrs = append(rowStrs, footer)
}
if len(rowStrs) == 0 {
return ""
}
body.WriteString(lipgloss.JoinVertical(lipgloss.Left, rowStrs...))
return body.String()
}