forked from toolshed/abra
chore: go mod tidy / vendor / make deps
This commit is contained in:
21
vendor/github.com/evertras/bubble-table/LICENSE
generated
vendored
Normal file
21
vendor/github.com/evertras/bubble-table/LICENSE
generated
vendored
Normal 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
439
vendor/github.com/evertras/bubble-table/table/border.go
generated
vendored
Normal 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
36
vendor/github.com/evertras/bubble-table/table/calc.go
generated
vendored
Normal 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
60
vendor/github.com/evertras/bubble-table/table/cell.go
generated
vendored
Normal 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
118
vendor/github.com/evertras/bubble-table/table/column.go
generated
vendored
Normal 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
67
vendor/github.com/evertras/bubble-table/table/data.go
generated
vendored
Normal 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
|
||||
}
|
116
vendor/github.com/evertras/bubble-table/table/dimensions.go
generated
vendored
Normal file
116
vendor/github.com/evertras/bubble-table/table/dimensions.go
generated
vendored
Normal 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
39
vendor/github.com/evertras/bubble-table/table/doc.go
generated
vendored
Normal 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
|
60
vendor/github.com/evertras/bubble-table/table/events.go
generated
vendored
Normal file
60
vendor/github.com/evertras/bubble-table/table/events.go
generated
vendored
Normal 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
164
vendor/github.com/evertras/bubble-table/table/filter.go
generated
vendored
Normal 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)
|
||||
}
|
51
vendor/github.com/evertras/bubble-table/table/footer.go
generated
vendored
Normal file
51
vendor/github.com/evertras/bubble-table/table/footer.go
generated
vendored
Normal 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)
|
||||
}
|
93
vendor/github.com/evertras/bubble-table/table/header.go
generated
vendored
Normal file
93
vendor/github.com/evertras/bubble-table/table/header.go
generated
vendored
Normal 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
120
vendor/github.com/evertras/bubble-table/table/keys.go
generated
vendored
Normal 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
148
vendor/github.com/evertras/bubble-table/table/model.go
generated
vendored
Normal 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
|
||||
}
|
510
vendor/github.com/evertras/bubble-table/table/options.go
generated
vendored
Normal file
510
vendor/github.com/evertras/bubble-table/table/options.go
generated
vendored
Normal 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
|
||||
}
|
18
vendor/github.com/evertras/bubble-table/table/overflow.go
generated
vendored
Normal file
18
vendor/github.com/evertras/bubble-table/table/overflow.go
generated
vendored
Normal 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)
|
||||
}
|
112
vendor/github.com/evertras/bubble-table/table/pagination.go
generated
vendored
Normal file
112
vendor/github.com/evertras/bubble-table/table/pagination.go
generated
vendored
Normal 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
96
vendor/github.com/evertras/bubble-table/table/query.go
generated
vendored
Normal 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
252
vendor/github.com/evertras/bubble-table/table/row.go
generated
vendored
Normal 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
|
||||
}
|
50
vendor/github.com/evertras/bubble-table/table/scrolling.go
generated
vendored
Normal file
50
vendor/github.com/evertras/bubble-table/table/scrolling.go
generated
vendored
Normal 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
178
vendor/github.com/evertras/bubble-table/table/sort.go
generated
vendored
Normal 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
|
||||
}
|
26
vendor/github.com/evertras/bubble-table/table/strlimit.go
generated
vendored
Normal file
26
vendor/github.com/evertras/bubble-table/table/strlimit.go
generated
vendored
Normal 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
154
vendor/github.com/evertras/bubble-table/table/update.go
generated
vendored
Normal 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
65
vendor/github.com/evertras/bubble-table/table/view.go
generated
vendored
Normal 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()
|
||||
}
|
Reference in New Issue
Block a user