416
cli/app/list.go
416
cli/app/list.go
@ -3,22 +3,22 @@ package app
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"slices"
|
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"coopcloud.tech/abra/cli/internal"
|
"coopcloud.tech/abra/cli/internal"
|
||||||
appPkg "coopcloud.tech/abra/pkg/app"
|
appPkg "coopcloud.tech/abra/pkg/app"
|
||||||
"coopcloud.tech/abra/pkg/autocomplete"
|
"coopcloud.tech/abra/pkg/autocomplete"
|
||||||
|
"coopcloud.tech/abra/pkg/config"
|
||||||
"coopcloud.tech/abra/pkg/i18n"
|
"coopcloud.tech/abra/pkg/i18n"
|
||||||
"coopcloud.tech/abra/pkg/log"
|
"coopcloud.tech/abra/pkg/log"
|
||||||
"coopcloud.tech/abra/pkg/recipe"
|
"coopcloud.tech/abra/pkg/recipe"
|
||||||
"coopcloud.tech/tagcmp"
|
"coopcloud.tech/tagcmp"
|
||||||
"github.com/charmbracelet/bubbles/key"
|
"github.com/charmbracelet/bubbles/key"
|
||||||
"github.com/charmbracelet/bubbles/spinner"
|
"github.com/charmbracelet/bubbles/list"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
"github.com/charmbracelet/lipgloss"
|
"github.com/charmbracelet/lipgloss"
|
||||||
"github.com/evertras/bubble-table/table"
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -56,15 +56,15 @@ var AppListCommand = &cobra.Command{
|
|||||||
Short: i18n.G("List all managed apps"),
|
Short: i18n.G("List all managed apps"),
|
||||||
Long: i18n.G(`Generate a report of all managed apps.
|
Long: i18n.G(`Generate a report of all managed apps.
|
||||||
|
|
||||||
Use "--status/-S" flag to query all servers for the live deployment status.`),
|
Use "status/S" flag to query all servers for the live deployment status.`),
|
||||||
Example: i18n.G(` # list apps of all servers without live status
|
Example: i18n.G(` # list apps of all servers without live status
|
||||||
abra app ls
|
abra app ls
|
||||||
|
|
||||||
# list apps of a specific server with live status
|
# list apps of a specific server with live status
|
||||||
abra app ls -s 1312.net -S
|
abra app ls s 1312.net S
|
||||||
|
|
||||||
# list apps of all servers which match a specific recipe
|
# list apps of all servers which match a specific recipe
|
||||||
abra app ls -r gitea`),
|
abra app ls r gitea`),
|
||||||
Args: cobra.NoArgs,
|
Args: cobra.NoArgs,
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
appFiles, err := appPkg.LoadAppFiles(listAppServer)
|
appFiles, err := appPkg.LoadAppFiles(listAppServer)
|
||||||
@ -221,7 +221,7 @@ Use "--status/-S" flag to query all servers for the live deployment status.`),
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := runTable(apps); err != nil {
|
if err := runTui(apps); err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -262,7 +262,7 @@ func init() {
|
|||||||
i18n.G("machine"),
|
i18n.G("machine"),
|
||||||
i18n.G("m"),
|
i18n.G("m"),
|
||||||
false,
|
false,
|
||||||
i18n.G("print machine-readable output"),
|
i18n.G("print machinereadable output"),
|
||||||
)
|
)
|
||||||
|
|
||||||
AppListCommand.Flags().StringVarP(
|
AppListCommand.Flags().StringVarP(
|
||||||
@ -281,45 +281,99 @@ func init() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getNumServersAndRecipes(apps []appPkg.App) (int, int) {
|
var listRenderStyle = lipgloss.NewStyle().Margin(1, 2)
|
||||||
var (
|
|
||||||
servers []string
|
|
||||||
recipes []string
|
|
||||||
)
|
|
||||||
|
|
||||||
for _, app := range apps {
|
type listItem struct {
|
||||||
if !slices.Contains(servers, app.Server) {
|
title string
|
||||||
servers = append(servers, app.Server)
|
app appPkg.App
|
||||||
}
|
appMetadata string
|
||||||
if !slices.Contains(recipes, app.Recipe.Name) {
|
|
||||||
recipes = append(recipes, app.Recipe.Name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return len(servers), len(recipes)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type errorMsg struct{ err error }
|
func (li listItem) Title() string { return li.title }
|
||||||
|
func (li listItem) Description() string { return li.appMetadata }
|
||||||
|
func (li listItem) FilterValue() string { return li.title + " " + li.appMetadata }
|
||||||
|
|
||||||
func (e errorMsg) Error() string { return e.err.Error() }
|
type model struct {
|
||||||
|
apps []appPkg.App
|
||||||
|
list list.Model
|
||||||
|
initStatusGather bool
|
||||||
|
pollingStatus bool
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m model) Init() tea.Cmd {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
var cmd tea.Cmd
|
||||||
|
var cmds []tea.Cmd
|
||||||
|
|
||||||
|
switch msg := msg.(type) {
|
||||||
|
case tea.KeyMsg:
|
||||||
|
switch msg.String() {
|
||||||
|
case "s":
|
||||||
|
if !m.pollingStatus {
|
||||||
|
m.pollingStatus = true
|
||||||
|
cmds = append(cmds, m.list.NewStatusMessage("Looking up app statuses 🧐"))
|
||||||
|
cmds = append(cmds, func() tea.Msg { return getAppsDeployStatus(m) })
|
||||||
|
} else {
|
||||||
|
cmds = append(cmds, m.list.NewStatusMessage("In progress, please hold ✋"))
|
||||||
|
}
|
||||||
|
case "q":
|
||||||
|
return m, tea.Quit
|
||||||
|
}
|
||||||
|
case appsDeployStatusMsg:
|
||||||
|
m.pollingStatus = false
|
||||||
|
cmds = append(cmds, m.list.SetItems(renderAppsDeployStatus(m, msg)))
|
||||||
|
case tea.WindowSizeMsg:
|
||||||
|
h, v := listRenderStyle.GetFrameSize()
|
||||||
|
m.list.SetSize(msg.Width-h, msg.Height-v)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.list, cmd = m.list.Update(msg)
|
||||||
|
cmds = append(cmds, cmd)
|
||||||
|
|
||||||
|
return m, tea.Batch(cmds...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m model) View() string {
|
||||||
|
return listRenderStyle.Render(m.list.View())
|
||||||
|
}
|
||||||
|
|
||||||
|
type listKeyMap struct {
|
||||||
|
status key.Binding
|
||||||
|
}
|
||||||
|
|
||||||
|
func newListKeyMap() *listKeyMap {
|
||||||
|
return &listKeyMap{
|
||||||
|
status: key.NewBinding(
|
||||||
|
key.WithKeys("s"),
|
||||||
|
key.WithHelp("s", "status"),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type appsDeployStatusMsg map[string]map[string]string
|
type appsDeployStatusMsg map[string]map[string]string
|
||||||
|
|
||||||
func getAppsDeployStatus(m model) tea.Msg {
|
func getAppsDeployStatus(m model) tea.Msg {
|
||||||
var apps []appPkg.App
|
var apps []appPkg.App
|
||||||
|
|
||||||
for _, row := range m.table.GetVisibleRows() {
|
for _, li := range m.list.VisibleItems() {
|
||||||
apps = append(apps, row.Data["app"].(appPkg.App))
|
i := li.(listItem) // NOTE(d1): convert back to our custom item
|
||||||
|
apps = append(apps, i.app)
|
||||||
}
|
}
|
||||||
|
|
||||||
statuses, err := appPkg.GetAppStatuses(apps, true)
|
statuses, err := appPkg.GetAppStatuses(apps, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorMsg{err}
|
errMsg := fmt.Sprintf("ERROR: %s", err.Error())
|
||||||
|
return func() tea.Msg { return m.list.NewStatusMessage(errMsg) }
|
||||||
}
|
}
|
||||||
|
|
||||||
catl, err := recipe.ReadRecipeCatalogue(true)
|
catl, err := recipe.ReadRecipeCatalogue(true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorMsg{err}
|
errMsg := fmt.Sprintf("ERROR: %s", err.Error())
|
||||||
|
return func() tea.Msg { return m.list.NewStatusMessage(errMsg) }
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, app := range apps {
|
for _, app := range apps {
|
||||||
@ -328,7 +382,8 @@ func getAppsDeployStatus(m model) tea.Msg {
|
|||||||
if version, ok := status["version"]; ok {
|
if version, ok := status["version"]; ok {
|
||||||
updates, err := recipe.GetRecipeCatalogueVersions(app.Recipe.Name, catl)
|
updates, err := recipe.GetRecipeCatalogueVersions(app.Recipe.Name, catl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorMsg{err}
|
errMsg := fmt.Sprintf("ERROR: %s", err.Error())
|
||||||
|
return func() tea.Msg { return m.list.NewStatusMessage(errMsg) }
|
||||||
}
|
}
|
||||||
|
|
||||||
parsedVersion, err := tagcmp.Parse(version)
|
parsedVersion, err := tagcmp.Parse(version)
|
||||||
@ -357,238 +412,115 @@ func getAppsDeployStatus(m model) tea.Msg {
|
|||||||
return appsDeployStatusMsg(statuses)
|
return appsDeployStatusMsg(statuses)
|
||||||
}
|
}
|
||||||
|
|
||||||
func renderAppsDeployStatus(m *model, appStatuses appsDeployStatusMsg) table.Model {
|
func renderAppsDeployStatus(m model, appStatuses appsDeployStatusMsg) []list.Item {
|
||||||
for _, row := range m.table.GetVisibleRows() {
|
itemsWithStatus := make(map[string]list.Item)
|
||||||
app := row.Data["app"].(appPkg.App)
|
|
||||||
appStatus := appStatuses[app.StackName()]
|
for _, li := range m.list.VisibleItems() {
|
||||||
|
i := li.(listItem) // NOTE(d1): convert back to our custom item
|
||||||
|
|
||||||
|
appStatus := appStatuses[i.app.StackName()]
|
||||||
|
|
||||||
var (
|
var (
|
||||||
version = appStatus["version"]
|
appVersion = appStatus["version"]
|
||||||
updates = appStatus["updates"]
|
appUpdates = appStatus["updates"]
|
||||||
status = appStatus["status"]
|
appDeploymentStatus = appStatus["status"]
|
||||||
chaos = appStatus["chaos"]
|
appChaosVersion = appStatus["chaosVersion"]
|
||||||
chaosVersion = appStatus["chaosVersion"]
|
|
||||||
autoUpdate = appStatus["autoUpdate"]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if status != "" {
|
if appVersion == "" {
|
||||||
row.Data["status"] = status
|
appVersion = "-"
|
||||||
}
|
}
|
||||||
|
|
||||||
if version != "" {
|
if appDeploymentStatus == "" || appDeploymentStatus == config.UNKNOWN_DEFAULT {
|
||||||
row.Data["version"] = version
|
appDeploymentStatus = "-"
|
||||||
row.Data["updates"] = updates
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if chaos != "" {
|
if appChaosVersion == "" {
|
||||||
if chaosVersion != "" {
|
appChaosVersion = "-"
|
||||||
row.Data["chaos-version"] = chaosVersion
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if autoUpdate != "" {
|
if len(appUpdates) == 0 {
|
||||||
row.Data["autoUpdate"] = autoUpdate
|
appUpdates = "-"
|
||||||
|
} else {
|
||||||
|
appUpdates = fmt.Sprintf("%d", len(appUpdates))
|
||||||
|
}
|
||||||
|
|
||||||
|
newMetadata := fmt.Sprintf(
|
||||||
|
"server: %s • recipe: %s • status: %s • version: %s • updates: %s • chaos: %s",
|
||||||
|
i.app.Server,
|
||||||
|
i.app.Recipe.Name,
|
||||||
|
appDeploymentStatus,
|
||||||
|
appVersion,
|
||||||
|
appUpdates,
|
||||||
|
appChaosVersion,
|
||||||
|
)
|
||||||
|
|
||||||
|
itemsWithStatus[i.title] = listItem{
|
||||||
|
title: i.title,
|
||||||
|
app: i.app,
|
||||||
|
appMetadata: newMetadata,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return m.table
|
|
||||||
}
|
|
||||||
|
|
||||||
type initTableMsg struct{ table table.Model }
|
var items []list.Item
|
||||||
|
for _, li := range m.list.Items() {
|
||||||
|
i := li.(listItem) // NOTE(d1): convert back to our custom item
|
||||||
|
|
||||||
func initTable(m model) tea.Msg {
|
if is, ok := itemsWithStatus[i.title]; ok {
|
||||||
var rows []table.Row
|
items = append(items, is)
|
||||||
|
continue
|
||||||
for _, app := range m.apps {
|
|
||||||
rows = append(rows, table.NewRow(table.RowData{
|
|
||||||
"domain": app.Domain,
|
|
||||||
"server": app.Server,
|
|
||||||
"recipe": app.Recipe.Name,
|
|
||||||
"app": app,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
colStyle := lipgloss.NewStyle().Align(lipgloss.Left)
|
|
||||||
columns := []table.Column{
|
|
||||||
table.NewFlexColumn("domain", "DOMAIN", 2).WithFiltered(true).WithStyle(colStyle),
|
|
||||||
table.NewFlexColumn("server", "SERVER", 1).WithFiltered(true).WithStyle(colStyle),
|
|
||||||
table.NewFlexColumn("recipe", "RECIPE", 1).WithFiltered(true).WithStyle(colStyle),
|
|
||||||
table.NewFlexColumn("status", "STATUS", 1).WithFiltered(true).WithStyle(colStyle),
|
|
||||||
table.NewFlexColumn("version", "VERSION", 1).WithFiltered(true).WithStyle(colStyle),
|
|
||||||
table.NewFlexColumn("updates", "UPDATES", 1).WithFiltered(true).WithStyle(colStyle),
|
|
||||||
table.NewFlexColumn("chaos-version", "CHAOS", 1).WithFiltered(true).WithStyle(colStyle),
|
|
||||||
table.NewFlexColumn("auto-update", "AUTO-UPDATE", 1).WithFiltered(true).WithStyle(colStyle),
|
|
||||||
}
|
|
||||||
|
|
||||||
keymap := table.DefaultKeyMap()
|
|
||||||
keymap.Filter = key.NewBinding(key.WithKeys("/", "f"))
|
|
||||||
keymap.PageDown = key.NewBinding(key.WithKeys("right", "l", "pgdown", "ctrl+d"))
|
|
||||||
keymap.PageUp = key.NewBinding(key.WithKeys("left", "h", "pgup", "ctrl+u"))
|
|
||||||
|
|
||||||
t := table.
|
|
||||||
New(columns).
|
|
||||||
Filtered(true).
|
|
||||||
Focused(true).
|
|
||||||
WithRows([]table.Row(rows)).
|
|
||||||
WithKeyMap(keymap).
|
|
||||||
WithMultiline(true).
|
|
||||||
WithFuzzyFilter().
|
|
||||||
SortByAsc("domain").
|
|
||||||
WithNoPagination().
|
|
||||||
WithMissingDataIndicatorStyled(table.StyledCell{
|
|
||||||
Style: lipgloss.NewStyle().Foreground(lipgloss.Color("#faa")),
|
|
||||||
Data: "-",
|
|
||||||
})
|
|
||||||
|
|
||||||
return initTableMsg{table: t}
|
|
||||||
}
|
|
||||||
|
|
||||||
type model struct {
|
|
||||||
apps []appPkg.App
|
|
||||||
|
|
||||||
numApps int
|
|
||||||
numServers int
|
|
||||||
numRecipes int
|
|
||||||
|
|
||||||
numFilteredApps int
|
|
||||||
numFilteredServers int
|
|
||||||
numFilteredRecipes int
|
|
||||||
|
|
||||||
initStatusGather bool
|
|
||||||
|
|
||||||
table table.Model
|
|
||||||
spinner spinner.Model
|
|
||||||
pollingStatus bool
|
|
||||||
|
|
||||||
width int
|
|
||||||
height int
|
|
||||||
|
|
||||||
err error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m model) getFilteredApps() []appPkg.App {
|
|
||||||
var servers []appPkg.App
|
|
||||||
for _, row := range m.table.GetVisibleRows() {
|
|
||||||
servers = append(servers, row.Data["app"].(appPkg.App))
|
|
||||||
}
|
|
||||||
return servers
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *model) updateCount() {
|
|
||||||
if m.table.GetIsFilterActive() {
|
|
||||||
apps := m.getFilteredApps()
|
|
||||||
m.numFilteredApps = len(apps)
|
|
||||||
m.numFilteredServers, m.numFilteredRecipes = getNumServersAndRecipes(apps)
|
|
||||||
} else {
|
|
||||||
m.numFilteredApps = m.numApps
|
|
||||||
m.numFilteredServers = m.numServers
|
|
||||||
m.numFilteredRecipes = m.numRecipes
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m model) Init() tea.Cmd {
|
|
||||||
return tea.Batch(
|
|
||||||
func() tea.Msg { return initTable(m) },
|
|
||||||
m.spinner.Tick,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
||||||
var cmd tea.Cmd
|
|
||||||
var cmds []tea.Cmd
|
|
||||||
|
|
||||||
switch msg := msg.(type) {
|
|
||||||
case tea.KeyMsg:
|
|
||||||
m.updateCount()
|
|
||||||
|
|
||||||
switch msg.String() {
|
|
||||||
case "q":
|
|
||||||
return m, tea.Quit
|
|
||||||
case "s":
|
|
||||||
if !m.table.GetIsFilterInputFocused() {
|
|
||||||
m.pollingStatus = true
|
|
||||||
return m, func() tea.Msg { return getAppsDeployStatus(m) }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
case initTableMsg:
|
|
||||||
m.table = msg.table
|
|
||||||
|
|
||||||
m.table = m.table.WithTargetWidth(m.width)
|
items = append(items, i)
|
||||||
m.table = m.table.WithPageSize(calculateHeight(m))
|
}
|
||||||
|
|
||||||
if m.initStatusGather {
|
return items
|
||||||
m.pollingStatus = true
|
}
|
||||||
return m, func() tea.Msg { return getAppsDeployStatus(m) }
|
|
||||||
|
func runTui(apps []appPkg.App) error {
|
||||||
|
var items []list.Item
|
||||||
|
|
||||||
|
sort.Sort(appPkg.ByServerAndDomain(apps))
|
||||||
|
|
||||||
|
for _, app := range apps {
|
||||||
|
items = append(items, listItem{
|
||||||
|
title: app.Domain,
|
||||||
|
app: app,
|
||||||
|
appMetadata: fmt.Sprintf(
|
||||||
|
"server: %s • recipe: %s • status: %s • version: %s • updates: %s • chaos: %s",
|
||||||
|
app.Server,
|
||||||
|
app.Recipe.Name,
|
||||||
|
"-",
|
||||||
|
"-",
|
||||||
|
"-",
|
||||||
|
"-",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate := list.NewDefaultDelegate()
|
||||||
|
delegateStyles := list.NewDefaultItemStyles()
|
||||||
|
delegateStyles.FilterMatch = delegateStyles.NormalTitle
|
||||||
|
delegate.Styles = delegateStyles
|
||||||
|
|
||||||
|
l := list.New(items, delegate, 0, 0)
|
||||||
|
l.Title = config.ABRA_DIR
|
||||||
|
l.StatusMessageLifetime = time.Second * 3
|
||||||
|
l.SetStatusBarItemName("app", "apps")
|
||||||
|
l.SetShowFilter(false)
|
||||||
|
|
||||||
|
listKeys := newListKeyMap()
|
||||||
|
l.AdditionalShortHelpKeys = func() []key.Binding {
|
||||||
|
return []key.Binding{
|
||||||
|
listKeys.status,
|
||||||
}
|
}
|
||||||
case appsDeployStatusMsg:
|
|
||||||
m.pollingStatus = false
|
|
||||||
m.table = renderAppsDeployStatus(&m, msg)
|
|
||||||
case tea.WindowSizeMsg:
|
|
||||||
m.width = msg.Width
|
|
||||||
m.height = msg.Height
|
|
||||||
|
|
||||||
m.table = m.table.WithTargetWidth(m.width)
|
|
||||||
m.table = m.table.WithPageSize(calculateHeight(m))
|
|
||||||
case errorMsg:
|
|
||||||
m.err = msg
|
|
||||||
}
|
}
|
||||||
|
|
||||||
m.table, cmd = m.table.Update(msg)
|
|
||||||
cmds = append(cmds, cmd)
|
|
||||||
|
|
||||||
m.spinner, cmd = m.spinner.Update(msg)
|
|
||||||
cmds = append(cmds, cmd)
|
|
||||||
|
|
||||||
return m, tea.Batch(cmds...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func calculateHeight(m model) int {
|
|
||||||
return m.height/2 - 5
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m model) View() string {
|
|
||||||
if m.err != nil {
|
|
||||||
return fmt.Sprintf("FATA: %v", m.err)
|
|
||||||
}
|
|
||||||
|
|
||||||
body := strings.Builder{}
|
|
||||||
|
|
||||||
body.WriteString(m.table.View() + "\n")
|
|
||||||
|
|
||||||
stats := fmt.Sprintf(
|
|
||||||
"[servers] %v • [apps] %v • [recipes] %v",
|
|
||||||
m.numFilteredServers, m.numFilteredApps, m.numFilteredRecipes,
|
|
||||||
)
|
|
||||||
|
|
||||||
help := "[q] quit • [/] filter • [s] status • [ctrl+u/d] page up/down"
|
|
||||||
|
|
||||||
body.WriteString(lipgloss.JoinHorizontal(lipgloss.Center, stats, " | ", help))
|
|
||||||
|
|
||||||
if m.pollingStatus {
|
|
||||||
body.WriteString(fmt.Sprintf(" | %s querying app status", m.spinner.View()))
|
|
||||||
} else {
|
|
||||||
body.WriteString(" | -")
|
|
||||||
}
|
|
||||||
|
|
||||||
return body.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func runTable(apps []appPkg.App) error {
|
|
||||||
s := spinner.New()
|
|
||||||
s.Spinner = spinner.Dot
|
|
||||||
s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("205"))
|
|
||||||
|
|
||||||
numServers, numRecipes := getNumServersAndRecipes(apps)
|
|
||||||
numApps := len(apps)
|
|
||||||
|
|
||||||
m := model{
|
m := model{
|
||||||
apps: apps,
|
apps: apps,
|
||||||
numApps: numApps,
|
list: l,
|
||||||
numServers: numServers,
|
initStatusGather: status,
|
||||||
numRecipes: numRecipes,
|
|
||||||
numFilteredApps: numApps,
|
|
||||||
numFilteredServers: numServers,
|
|
||||||
numFilteredRecipes: numRecipes,
|
|
||||||
spinner: s,
|
|
||||||
initStatusGather: status,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
p := tea.NewProgram(m, tea.WithAltScreen())
|
p := tea.NewProgram(m, tea.WithAltScreen())
|
||||||
|
|||||||
3
go.mod
3
go.mod
@ -16,7 +16,6 @@ require (
|
|||||||
github.com/docker/cli v28.4.0+incompatible
|
github.com/docker/cli v28.4.0+incompatible
|
||||||
github.com/docker/docker v28.4.0+incompatible
|
github.com/docker/docker v28.4.0+incompatible
|
||||||
github.com/docker/go-units v0.5.0
|
github.com/docker/go-units v0.5.0
|
||||||
github.com/evertras/bubble-table v0.19.2
|
|
||||||
github.com/go-git/go-git/v5 v5.16.2
|
github.com/go-git/go-git/v5 v5.16.2
|
||||||
github.com/google/go-cmp v0.7.0
|
github.com/google/go-cmp v0.7.0
|
||||||
github.com/leonelquinteros/gotext v1.7.2
|
github.com/leonelquinteros/gotext v1.7.2
|
||||||
@ -94,7 +93,6 @@ require (
|
|||||||
github.com/morikuni/aec v1.0.0 // indirect
|
github.com/morikuni/aec v1.0.0 // indirect
|
||||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||||
github.com/muesli/reflow v0.3.0 // indirect
|
|
||||||
github.com/muesli/termenv v0.16.0 // indirect
|
github.com/muesli/termenv v0.16.0 // indirect
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||||
@ -107,6 +105,7 @@ require (
|
|||||||
github.com/prometheus/procfs v0.17.0 // indirect
|
github.com/prometheus/procfs v0.17.0 // indirect
|
||||||
github.com/rivo/uniseg v0.4.7 // indirect
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||||
|
github.com/sahilm/fuzzy v0.1.1 // indirect
|
||||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||||
github.com/skeema/knownhosts v1.3.1 // indirect
|
github.com/skeema/knownhosts v1.3.1 // indirect
|
||||||
github.com/spf13/pflag v1.0.10 // indirect
|
github.com/spf13/pflag v1.0.10 // indirect
|
||||||
|
|||||||
9
go.sum
9
go.sum
@ -373,8 +373,6 @@ github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6
|
|||||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
||||||
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
|
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
|
||||||
github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
|
github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
|
||||||
github.com/evertras/bubble-table v0.19.2 h1:u77oiM6JlRR+CvS5FZc3Hz+J6iEsvEDcR5kO8OFb1Yw=
|
|
||||||
github.com/evertras/bubble-table v0.19.2/go.mod h1:ifHujS1YxwnYSOgcR2+m3GnJ84f7CVU/4kUOxUCjEbQ=
|
|
||||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||||
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
|
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
|
||||||
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
|
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
|
||||||
@ -640,7 +638,6 @@ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D
|
|||||||
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
||||||
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
||||||
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||||
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
|
||||||
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
|
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
|
||||||
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||||
github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=
|
github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=
|
||||||
@ -702,8 +699,6 @@ github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D
|
|||||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
|
||||||
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||||
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||||
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
|
|
||||||
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
|
|
||||||
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
||||||
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
||||||
github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||||
@ -818,8 +813,6 @@ github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1
|
|||||||
github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0=
|
github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0=
|
||||||
github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=
|
github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=
|
||||||
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
||||||
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
|
||||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
|
||||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||||
@ -831,6 +824,8 @@ github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD
|
|||||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4=
|
github.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4=
|
||||||
|
github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA=
|
||||||
|
github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
|
||||||
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||||
github.com/schollz/progressbar/v3 v3.18.0 h1:uXdoHABRFmNIjUfte/Ex7WtuyVslrw2wVPQmCN62HpA=
|
github.com/schollz/progressbar/v3 v3.18.0 h1:uXdoHABRFmNIjUfte/Ex7WtuyVslrw2wVPQmCN62HpA=
|
||||||
github.com/schollz/progressbar/v3 v3.18.0/go.mod h1:IsO3lpbaGuzh8zIMzgY3+J8l4C8GjO0Y9S69eFvNsec=
|
github.com/schollz/progressbar/v3 v3.18.0/go.mod h1:IsO3lpbaGuzh8zIMzgY3+J8l4C8GjO0Y9S69eFvNsec=
|
||||||
|
|||||||
@ -224,6 +224,18 @@ func (a ByServerAndRecipe) Less(i, j int) bool {
|
|||||||
return strings.ToLower(a[i].Server) < strings.ToLower(a[j].Server)
|
return strings.ToLower(a[i].Server) < strings.ToLower(a[j].Server)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ByServerAndRecipe sort a slice of Apps
|
||||||
|
type ByServerAndDomain []App
|
||||||
|
|
||||||
|
func (a ByServerAndDomain) Len() int { return len(a) }
|
||||||
|
func (a ByServerAndDomain) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||||
|
func (a ByServerAndDomain) Less(i, j int) bool {
|
||||||
|
if a[i].Server == a[j].Server {
|
||||||
|
return strings.ToLower(a[i].Domain) < strings.ToLower(a[j].Domain)
|
||||||
|
}
|
||||||
|
return strings.ToLower(a[i].Server) < strings.ToLower(a[j].Server)
|
||||||
|
}
|
||||||
|
|
||||||
// ByRecipe sort a slice of Apps
|
// ByRecipe sort a slice of Apps
|
||||||
type ByRecipe []App
|
type ByRecipe []App
|
||||||
|
|
||||||
|
|||||||
240
vendor/github.com/charmbracelet/bubbles/help/help.go
generated
vendored
Normal file
240
vendor/github.com/charmbracelet/bubbles/help/help.go
generated
vendored
Normal file
@ -0,0 +1,240 @@
|
|||||||
|
// Package help provides a simple help view for Bubble Tea applications.
|
||||||
|
package help
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/bubbles/key"
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
)
|
||||||
|
|
||||||
|
// KeyMap is a map of keybindings used to generate help. Since it's an
|
||||||
|
// interface it can be any type, though struct or a map[string][]key.Binding
|
||||||
|
// are likely candidates.
|
||||||
|
//
|
||||||
|
// Note that if a key is disabled (via key.Binding.SetEnabled) it will not be
|
||||||
|
// rendered in the help view, so in theory generated help should self-manage.
|
||||||
|
type KeyMap interface {
|
||||||
|
// ShortHelp returns a slice of bindings to be displayed in the short
|
||||||
|
// version of the help. The help bubble will render help in the order in
|
||||||
|
// which the help items are returned here.
|
||||||
|
ShortHelp() []key.Binding
|
||||||
|
|
||||||
|
// FullHelp returns an extended group of help items, grouped by columns.
|
||||||
|
// The help bubble will render the help in the order in which the help
|
||||||
|
// items are returned here.
|
||||||
|
FullHelp() [][]key.Binding
|
||||||
|
}
|
||||||
|
|
||||||
|
// Styles is a set of available style definitions for the Help bubble.
|
||||||
|
type Styles struct {
|
||||||
|
Ellipsis lipgloss.Style
|
||||||
|
|
||||||
|
// Styling for the short help
|
||||||
|
ShortKey lipgloss.Style
|
||||||
|
ShortDesc lipgloss.Style
|
||||||
|
ShortSeparator lipgloss.Style
|
||||||
|
|
||||||
|
// Styling for the full help
|
||||||
|
FullKey lipgloss.Style
|
||||||
|
FullDesc lipgloss.Style
|
||||||
|
FullSeparator lipgloss.Style
|
||||||
|
}
|
||||||
|
|
||||||
|
// Model contains the state of the help view.
|
||||||
|
type Model struct {
|
||||||
|
Width int
|
||||||
|
ShowAll bool // if true, render the "full" help menu
|
||||||
|
|
||||||
|
ShortSeparator string
|
||||||
|
FullSeparator string
|
||||||
|
|
||||||
|
// The symbol we use in the short help when help items have been truncated
|
||||||
|
// due to width. Periods of ellipsis by default.
|
||||||
|
Ellipsis string
|
||||||
|
|
||||||
|
Styles Styles
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a new help view with some useful defaults.
|
||||||
|
func New() Model {
|
||||||
|
keyStyle := lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{
|
||||||
|
Light: "#909090",
|
||||||
|
Dark: "#626262",
|
||||||
|
})
|
||||||
|
|
||||||
|
descStyle := lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{
|
||||||
|
Light: "#B2B2B2",
|
||||||
|
Dark: "#4A4A4A",
|
||||||
|
})
|
||||||
|
|
||||||
|
sepStyle := lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{
|
||||||
|
Light: "#DDDADA",
|
||||||
|
Dark: "#3C3C3C",
|
||||||
|
})
|
||||||
|
|
||||||
|
return Model{
|
||||||
|
ShortSeparator: " • ",
|
||||||
|
FullSeparator: " ",
|
||||||
|
Ellipsis: "…",
|
||||||
|
Styles: Styles{
|
||||||
|
ShortKey: keyStyle,
|
||||||
|
ShortDesc: descStyle,
|
||||||
|
ShortSeparator: sepStyle,
|
||||||
|
Ellipsis: sepStyle,
|
||||||
|
FullKey: keyStyle,
|
||||||
|
FullDesc: descStyle,
|
||||||
|
FullSeparator: sepStyle,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewModel creates a new help view with some useful defaults.
|
||||||
|
//
|
||||||
|
// Deprecated: use [New] instead.
|
||||||
|
var NewModel = New
|
||||||
|
|
||||||
|
// Update helps satisfy the Bubble Tea Model interface. It's a no-op.
|
||||||
|
func (m Model) Update(_ tea.Msg) (Model, tea.Cmd) {
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// View renders the help view's current state.
|
||||||
|
func (m Model) View(k KeyMap) string {
|
||||||
|
if m.ShowAll {
|
||||||
|
return m.FullHelpView(k.FullHelp())
|
||||||
|
}
|
||||||
|
return m.ShortHelpView(k.ShortHelp())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShortHelpView renders a single line help view from a slice of keybindings.
|
||||||
|
// If the line is longer than the maximum width it will be gracefully
|
||||||
|
// truncated, showing only as many help items as possible.
|
||||||
|
func (m Model) ShortHelpView(bindings []key.Binding) string {
|
||||||
|
if len(bindings) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
var b strings.Builder
|
||||||
|
var totalWidth int
|
||||||
|
separator := m.Styles.ShortSeparator.Inline(true).Render(m.ShortSeparator)
|
||||||
|
|
||||||
|
for i, kb := range bindings {
|
||||||
|
if !kb.Enabled() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sep
|
||||||
|
var sep string
|
||||||
|
if totalWidth > 0 && i < len(bindings) {
|
||||||
|
sep = separator
|
||||||
|
}
|
||||||
|
|
||||||
|
// Item
|
||||||
|
str := sep +
|
||||||
|
m.Styles.ShortKey.Inline(true).Render(kb.Help().Key) + " " +
|
||||||
|
m.Styles.ShortDesc.Inline(true).Render(kb.Help().Desc)
|
||||||
|
w := lipgloss.Width(str)
|
||||||
|
|
||||||
|
// Tail
|
||||||
|
if tail, ok := m.shouldAddItem(totalWidth, w); !ok {
|
||||||
|
if tail != "" {
|
||||||
|
b.WriteString(tail)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
totalWidth += w
|
||||||
|
b.WriteString(str)
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// FullHelpView renders help columns from a slice of key binding slices. Each
|
||||||
|
// top level slice entry renders into a column.
|
||||||
|
func (m Model) FullHelpView(groups [][]key.Binding) string {
|
||||||
|
if len(groups) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Linter note: at this time we don't think it's worth the additional
|
||||||
|
// code complexity involved in preallocating this slice.
|
||||||
|
//nolint:prealloc
|
||||||
|
var (
|
||||||
|
out []string
|
||||||
|
|
||||||
|
totalWidth int
|
||||||
|
separator = m.Styles.FullSeparator.Inline(true).Render(m.FullSeparator)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Iterate over groups to build columns
|
||||||
|
for i, group := range groups {
|
||||||
|
if group == nil || !shouldRenderColumn(group) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var (
|
||||||
|
sep string
|
||||||
|
keys []string
|
||||||
|
descriptions []string
|
||||||
|
)
|
||||||
|
|
||||||
|
// Sep
|
||||||
|
if totalWidth > 0 && i < len(groups) {
|
||||||
|
sep = separator
|
||||||
|
}
|
||||||
|
|
||||||
|
// Separate keys and descriptions into different slices
|
||||||
|
for _, kb := range group {
|
||||||
|
if !kb.Enabled() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
keys = append(keys, kb.Help().Key)
|
||||||
|
descriptions = append(descriptions, kb.Help().Desc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Column
|
||||||
|
col := lipgloss.JoinHorizontal(lipgloss.Top,
|
||||||
|
sep,
|
||||||
|
m.Styles.FullKey.Render(strings.Join(keys, "\n")),
|
||||||
|
" ",
|
||||||
|
m.Styles.FullDesc.Render(strings.Join(descriptions, "\n")),
|
||||||
|
)
|
||||||
|
w := lipgloss.Width(col)
|
||||||
|
|
||||||
|
// Tail
|
||||||
|
if tail, ok := m.shouldAddItem(totalWidth, w); !ok {
|
||||||
|
if tail != "" {
|
||||||
|
out = append(out, tail)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
totalWidth += w
|
||||||
|
out = append(out, col)
|
||||||
|
}
|
||||||
|
|
||||||
|
return lipgloss.JoinHorizontal(lipgloss.Top, out...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Model) shouldAddItem(totalWidth, width int) (tail string, ok bool) {
|
||||||
|
// If there's room for an ellipsis, print that.
|
||||||
|
if m.Width > 0 && totalWidth+width > m.Width {
|
||||||
|
tail = " " + m.Styles.Ellipsis.Inline(true).Render(m.Ellipsis)
|
||||||
|
|
||||||
|
if totalWidth+lipgloss.Width(tail) < m.Width {
|
||||||
|
return tail, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", true
|
||||||
|
}
|
||||||
|
|
||||||
|
func shouldRenderColumn(b []key.Binding) (ok bool) {
|
||||||
|
for _, v := range b {
|
||||||
|
if v.Enabled() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
71
vendor/github.com/charmbracelet/bubbles/list/README.md
generated
vendored
Normal file
71
vendor/github.com/charmbracelet/bubbles/list/README.md
generated
vendored
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
# Frequently Asked Questions
|
||||||
|
|
||||||
|
These are some of the most commonly asked questions regarding the `list` bubble.
|
||||||
|
|
||||||
|
## Adding Custom Items
|
||||||
|
|
||||||
|
There are a few things you need to do to create custom items. First off, they
|
||||||
|
need to implement the `list.Item` and `list.DefaultItem` interfaces.
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Item is an item that appears in the list.
|
||||||
|
type Item interface {
|
||||||
|
// FilterValue is the value we use when filtering against this item when
|
||||||
|
// we're filtering the list.
|
||||||
|
FilterValue() string
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```go
|
||||||
|
// DefaultItem describes an item designed to work with DefaultDelegate.
|
||||||
|
type DefaultItem interface {
|
||||||
|
Item
|
||||||
|
Title() string
|
||||||
|
Description() string
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
You can see a working example in our [Kancli][kancli] project built
|
||||||
|
explicitly for a tutorial on lists and composite views in Bubble Tea.
|
||||||
|
|
||||||
|
[VIDEO](https://youtu.be/ZA93qgdLUzM)
|
||||||
|
|
||||||
|
## Customizing Styles
|
||||||
|
|
||||||
|
Rendering (and behavior) for list items is done via the
|
||||||
|
[`ItemDelegate`][itemDelegate]
|
||||||
|
interface. It can be a little confusing at first, but it allows the list to be
|
||||||
|
very flexible and powerful.
|
||||||
|
|
||||||
|
If you just want to alter the default style you could do something like:
|
||||||
|
|
||||||
|
```go
|
||||||
|
import "github.com/charmbracelet/bubbles/list"
|
||||||
|
|
||||||
|
// Create a new default delegate
|
||||||
|
d := list.NewDefaultDelegate()
|
||||||
|
|
||||||
|
// Change colors
|
||||||
|
c := lipgloss.Color("#6f03fc")
|
||||||
|
d.Styles.SelectedTitle = d.Styles.SelectedTitle.Foreground(c).BorderLeftForeground(c)
|
||||||
|
d.Styles.SelectedDesc = d.Styles.SelectedTitle.Copy() // reuse the title style here
|
||||||
|
|
||||||
|
// Initailize the list model with our delegate
|
||||||
|
width, height := 80, 40
|
||||||
|
l := list.New(listItems, d, width, height)
|
||||||
|
|
||||||
|
// You can also change the delegate on the fly
|
||||||
|
l.SetDelegate(d)
|
||||||
|
```
|
||||||
|
|
||||||
|
This code would replace [this line][replacedLine] in the [`list-default`
|
||||||
|
example][listDefault].
|
||||||
|
|
||||||
|
For full control over the way list items are rendered you can also define your
|
||||||
|
own `ItemDelegate` too ([example][customDelegate]).
|
||||||
|
|
||||||
|
[kancli]: https://github.com/charmbracelet/kancli/blob/main/main.go#L45
|
||||||
|
[itemDelegate]: https://pkg.go.dev/github.com/charmbracelet/bubbles/list#ItemDelegate
|
||||||
|
[replacedLine]: https://github.com/charmbracelet/bubbletea/blob/main/examples/list-default/main.go#L77
|
||||||
|
[listDefault]: https://github.com/charmbracelet/bubbletea/tree/main/examples/list-default
|
||||||
|
[customDelegate]: https://github.com/charmbracelet/bubbletea/blob/main/examples/list-simple/main.go#L29-L50
|
||||||
229
vendor/github.com/charmbracelet/bubbles/list/defaultitem.go
generated
vendored
Normal file
229
vendor/github.com/charmbracelet/bubbles/list/defaultitem.go
generated
vendored
Normal file
@ -0,0 +1,229 @@
|
|||||||
|
package list
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/bubbles/key"
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
"github.com/charmbracelet/x/ansi"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DefaultItemStyles defines styling for a default list item.
|
||||||
|
// See DefaultItemView for when these come into play.
|
||||||
|
type DefaultItemStyles struct {
|
||||||
|
// The Normal state.
|
||||||
|
NormalTitle lipgloss.Style
|
||||||
|
NormalDesc lipgloss.Style
|
||||||
|
|
||||||
|
// The selected item state.
|
||||||
|
SelectedTitle lipgloss.Style
|
||||||
|
SelectedDesc lipgloss.Style
|
||||||
|
|
||||||
|
// The dimmed state, for when the filter input is initially activated.
|
||||||
|
DimmedTitle lipgloss.Style
|
||||||
|
DimmedDesc lipgloss.Style
|
||||||
|
|
||||||
|
// Characters matching the current filter, if any.
|
||||||
|
FilterMatch lipgloss.Style
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDefaultItemStyles returns style definitions for a default item. See
|
||||||
|
// DefaultItemView for when these come into play.
|
||||||
|
func NewDefaultItemStyles() (s DefaultItemStyles) {
|
||||||
|
s.NormalTitle = lipgloss.NewStyle().
|
||||||
|
Foreground(lipgloss.AdaptiveColor{Light: "#1a1a1a", Dark: "#dddddd"}).
|
||||||
|
Padding(0, 0, 0, 2) //nolint:mnd
|
||||||
|
|
||||||
|
s.NormalDesc = s.NormalTitle.
|
||||||
|
Foreground(lipgloss.AdaptiveColor{Light: "#A49FA5", Dark: "#777777"})
|
||||||
|
|
||||||
|
s.SelectedTitle = lipgloss.NewStyle().
|
||||||
|
Border(lipgloss.NormalBorder(), false, false, false, true).
|
||||||
|
BorderForeground(lipgloss.AdaptiveColor{Light: "#F793FF", Dark: "#AD58B4"}).
|
||||||
|
Foreground(lipgloss.AdaptiveColor{Light: "#EE6FF8", Dark: "#EE6FF8"}).
|
||||||
|
Padding(0, 0, 0, 1)
|
||||||
|
|
||||||
|
s.SelectedDesc = s.SelectedTitle.
|
||||||
|
Foreground(lipgloss.AdaptiveColor{Light: "#F793FF", Dark: "#AD58B4"})
|
||||||
|
|
||||||
|
s.DimmedTitle = lipgloss.NewStyle().
|
||||||
|
Foreground(lipgloss.AdaptiveColor{Light: "#A49FA5", Dark: "#777777"}).
|
||||||
|
Padding(0, 0, 0, 2) //nolint:mnd
|
||||||
|
|
||||||
|
s.DimmedDesc = s.DimmedTitle.
|
||||||
|
Foreground(lipgloss.AdaptiveColor{Light: "#C2B8C2", Dark: "#4D4D4D"})
|
||||||
|
|
||||||
|
s.FilterMatch = lipgloss.NewStyle().Underline(true)
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultItem describes an item designed to work with DefaultDelegate.
|
||||||
|
type DefaultItem interface {
|
||||||
|
Item
|
||||||
|
Title() string
|
||||||
|
Description() string
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultDelegate is a standard delegate designed to work in lists. It's
|
||||||
|
// styled by DefaultItemStyles, which can be customized as you like.
|
||||||
|
//
|
||||||
|
// The description line can be hidden by setting Description to false, which
|
||||||
|
// renders the list as single-line-items. The spacing between items can be set
|
||||||
|
// with the SetSpacing method.
|
||||||
|
//
|
||||||
|
// Setting UpdateFunc is optional. If it's set it will be called when the
|
||||||
|
// ItemDelegate called, which is called when the list's Update function is
|
||||||
|
// invoked.
|
||||||
|
//
|
||||||
|
// Settings ShortHelpFunc and FullHelpFunc is optional. They can be set to
|
||||||
|
// include items in the list's default short and full help menus.
|
||||||
|
type DefaultDelegate struct {
|
||||||
|
ShowDescription bool
|
||||||
|
Styles DefaultItemStyles
|
||||||
|
UpdateFunc func(tea.Msg, *Model) tea.Cmd
|
||||||
|
ShortHelpFunc func() []key.Binding
|
||||||
|
FullHelpFunc func() [][]key.Binding
|
||||||
|
height int
|
||||||
|
spacing int
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDefaultDelegate creates a new delegate with default styles.
|
||||||
|
func NewDefaultDelegate() DefaultDelegate {
|
||||||
|
const defaultHeight = 2
|
||||||
|
const defaultSpacing = 1
|
||||||
|
return DefaultDelegate{
|
||||||
|
ShowDescription: true,
|
||||||
|
Styles: NewDefaultItemStyles(),
|
||||||
|
height: defaultHeight,
|
||||||
|
spacing: defaultSpacing,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetHeight sets delegate's preferred height.
|
||||||
|
func (d *DefaultDelegate) SetHeight(i int) {
|
||||||
|
d.height = i
|
||||||
|
}
|
||||||
|
|
||||||
|
// Height returns the delegate's preferred height.
|
||||||
|
// This has effect only if ShowDescription is true,
|
||||||
|
// otherwise height is always 1.
|
||||||
|
func (d DefaultDelegate) Height() int {
|
||||||
|
if d.ShowDescription {
|
||||||
|
return d.height
|
||||||
|
}
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetSpacing sets the delegate's spacing.
|
||||||
|
func (d *DefaultDelegate) SetSpacing(i int) {
|
||||||
|
d.spacing = i
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spacing returns the delegate's spacing.
|
||||||
|
func (d DefaultDelegate) Spacing() int {
|
||||||
|
return d.spacing
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update checks whether the delegate's UpdateFunc is set and calls it.
|
||||||
|
func (d DefaultDelegate) Update(msg tea.Msg, m *Model) tea.Cmd {
|
||||||
|
if d.UpdateFunc == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return d.UpdateFunc(msg, m)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render prints an item.
|
||||||
|
func (d DefaultDelegate) Render(w io.Writer, m Model, index int, item Item) {
|
||||||
|
var (
|
||||||
|
title, desc string
|
||||||
|
matchedRunes []int
|
||||||
|
s = &d.Styles
|
||||||
|
)
|
||||||
|
|
||||||
|
if i, ok := item.(DefaultItem); ok {
|
||||||
|
title = i.Title()
|
||||||
|
desc = i.Description()
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.width <= 0 {
|
||||||
|
// short-circuit
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent text from exceeding list width
|
||||||
|
textwidth := m.width - s.NormalTitle.GetPaddingLeft() - s.NormalTitle.GetPaddingRight()
|
||||||
|
title = ansi.Truncate(title, textwidth, ellipsis)
|
||||||
|
if d.ShowDescription {
|
||||||
|
var lines []string
|
||||||
|
for i, line := range strings.Split(desc, "\n") {
|
||||||
|
if i >= d.height-1 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
lines = append(lines, ansi.Truncate(line, textwidth, ellipsis))
|
||||||
|
}
|
||||||
|
desc = strings.Join(lines, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Conditions
|
||||||
|
var (
|
||||||
|
isSelected = index == m.Index()
|
||||||
|
emptyFilter = m.FilterState() == Filtering && m.FilterValue() == ""
|
||||||
|
isFiltered = m.FilterState() == Filtering || m.FilterState() == FilterApplied
|
||||||
|
)
|
||||||
|
|
||||||
|
if isFiltered && index < len(m.filteredItems) {
|
||||||
|
// Get indices of matched characters
|
||||||
|
matchedRunes = m.MatchesForItem(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
if emptyFilter {
|
||||||
|
title = s.DimmedTitle.Render(title)
|
||||||
|
desc = s.DimmedDesc.Render(desc)
|
||||||
|
} else if isSelected && m.FilterState() != Filtering {
|
||||||
|
if isFiltered {
|
||||||
|
// Highlight matches
|
||||||
|
unmatched := s.SelectedTitle.Inline(true)
|
||||||
|
matched := unmatched.Inherit(s.FilterMatch)
|
||||||
|
title = lipgloss.StyleRunes(title, matchedRunes, matched, unmatched)
|
||||||
|
}
|
||||||
|
title = s.SelectedTitle.Render(title)
|
||||||
|
desc = s.SelectedDesc.Render(desc)
|
||||||
|
} else {
|
||||||
|
if isFiltered {
|
||||||
|
// Highlight matches
|
||||||
|
unmatched := s.NormalTitle.Inline(true)
|
||||||
|
matched := unmatched.Inherit(s.FilterMatch)
|
||||||
|
title = lipgloss.StyleRunes(title, matchedRunes, matched, unmatched)
|
||||||
|
}
|
||||||
|
title = s.NormalTitle.Render(title)
|
||||||
|
desc = s.NormalDesc.Render(desc)
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.ShowDescription {
|
||||||
|
fmt.Fprintf(w, "%s\n%s", title, desc) //nolint: errcheck
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Fprintf(w, "%s", title) //nolint: errcheck
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShortHelp returns the delegate's short help.
|
||||||
|
func (d DefaultDelegate) ShortHelp() []key.Binding {
|
||||||
|
if d.ShortHelpFunc != nil {
|
||||||
|
return d.ShortHelpFunc()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FullHelp returns the delegate's full help.
|
||||||
|
func (d DefaultDelegate) FullHelp() [][]key.Binding {
|
||||||
|
if d.FullHelpFunc != nil {
|
||||||
|
return d.FullHelpFunc()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
97
vendor/github.com/charmbracelet/bubbles/list/keys.go
generated
vendored
Normal file
97
vendor/github.com/charmbracelet/bubbles/list/keys.go
generated
vendored
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
package list
|
||||||
|
|
||||||
|
import "github.com/charmbracelet/bubbles/key"
|
||||||
|
|
||||||
|
// KeyMap defines keybindings. It satisfies to the help.KeyMap interface, which
|
||||||
|
// is used to render the menu.
|
||||||
|
type KeyMap struct {
|
||||||
|
// Keybindings used when browsing the list.
|
||||||
|
CursorUp key.Binding
|
||||||
|
CursorDown key.Binding
|
||||||
|
NextPage key.Binding
|
||||||
|
PrevPage key.Binding
|
||||||
|
GoToStart key.Binding
|
||||||
|
GoToEnd key.Binding
|
||||||
|
Filter key.Binding
|
||||||
|
ClearFilter key.Binding
|
||||||
|
|
||||||
|
// Keybindings used when setting a filter.
|
||||||
|
CancelWhileFiltering key.Binding
|
||||||
|
AcceptWhileFiltering key.Binding
|
||||||
|
|
||||||
|
// Help toggle keybindings.
|
||||||
|
ShowFullHelp key.Binding
|
||||||
|
CloseFullHelp key.Binding
|
||||||
|
|
||||||
|
// The quit keybinding. This won't be caught when filtering.
|
||||||
|
Quit key.Binding
|
||||||
|
|
||||||
|
// The quit-no-matter-what keybinding. This will be caught when filtering.
|
||||||
|
ForceQuit key.Binding
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultKeyMap returns a default set of keybindings.
|
||||||
|
func DefaultKeyMap() KeyMap {
|
||||||
|
return KeyMap{
|
||||||
|
// Browsing.
|
||||||
|
CursorUp: key.NewBinding(
|
||||||
|
key.WithKeys("up", "k"),
|
||||||
|
key.WithHelp("↑/k", "up"),
|
||||||
|
),
|
||||||
|
CursorDown: key.NewBinding(
|
||||||
|
key.WithKeys("down", "j"),
|
||||||
|
key.WithHelp("↓/j", "down"),
|
||||||
|
),
|
||||||
|
PrevPage: key.NewBinding(
|
||||||
|
key.WithKeys("left", "h", "pgup", "b", "u"),
|
||||||
|
key.WithHelp("←/h/pgup", "prev page"),
|
||||||
|
),
|
||||||
|
NextPage: key.NewBinding(
|
||||||
|
key.WithKeys("right", "l", "pgdown", "f", "d"),
|
||||||
|
key.WithHelp("→/l/pgdn", "next page"),
|
||||||
|
),
|
||||||
|
GoToStart: key.NewBinding(
|
||||||
|
key.WithKeys("home", "g"),
|
||||||
|
key.WithHelp("g/home", "go to start"),
|
||||||
|
),
|
||||||
|
GoToEnd: key.NewBinding(
|
||||||
|
key.WithKeys("end", "G"),
|
||||||
|
key.WithHelp("G/end", "go to end"),
|
||||||
|
),
|
||||||
|
Filter: key.NewBinding(
|
||||||
|
key.WithKeys("/"),
|
||||||
|
key.WithHelp("/", "filter"),
|
||||||
|
),
|
||||||
|
ClearFilter: key.NewBinding(
|
||||||
|
key.WithKeys("esc"),
|
||||||
|
key.WithHelp("esc", "clear filter"),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Filtering.
|
||||||
|
CancelWhileFiltering: key.NewBinding(
|
||||||
|
key.WithKeys("esc"),
|
||||||
|
key.WithHelp("esc", "cancel"),
|
||||||
|
),
|
||||||
|
AcceptWhileFiltering: key.NewBinding(
|
||||||
|
key.WithKeys("enter", "tab", "shift+tab", "ctrl+k", "up", "ctrl+j", "down"),
|
||||||
|
key.WithHelp("enter", "apply filter"),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Toggle help.
|
||||||
|
ShowFullHelp: key.NewBinding(
|
||||||
|
key.WithKeys("?"),
|
||||||
|
key.WithHelp("?", "more"),
|
||||||
|
),
|
||||||
|
CloseFullHelp: key.NewBinding(
|
||||||
|
key.WithKeys("?"),
|
||||||
|
key.WithHelp("?", "close help"),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Quitting.
|
||||||
|
Quit: key.NewBinding(
|
||||||
|
key.WithKeys("q", "esc"),
|
||||||
|
key.WithHelp("q", "quit"),
|
||||||
|
),
|
||||||
|
ForceQuit: key.NewBinding(key.WithKeys("ctrl+c")),
|
||||||
|
}
|
||||||
|
}
|
||||||
1328
vendor/github.com/charmbracelet/bubbles/list/list.go
generated
vendored
Normal file
1328
vendor/github.com/charmbracelet/bubbles/list/list.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
99
vendor/github.com/charmbracelet/bubbles/list/style.go
generated
vendored
Normal file
99
vendor/github.com/charmbracelet/bubbles/list/style.go
generated
vendored
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
package list
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
bullet = "•"
|
||||||
|
ellipsis = "…"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Styles contains style definitions for this list component. By default, these
|
||||||
|
// values are generated by DefaultStyles.
|
||||||
|
type Styles struct {
|
||||||
|
TitleBar lipgloss.Style
|
||||||
|
Title lipgloss.Style
|
||||||
|
Spinner lipgloss.Style
|
||||||
|
FilterPrompt lipgloss.Style
|
||||||
|
FilterCursor lipgloss.Style
|
||||||
|
|
||||||
|
// Default styling for matched characters in a filter. This can be
|
||||||
|
// overridden by delegates.
|
||||||
|
DefaultFilterCharacterMatch lipgloss.Style
|
||||||
|
|
||||||
|
StatusBar lipgloss.Style
|
||||||
|
StatusEmpty lipgloss.Style
|
||||||
|
StatusBarActiveFilter lipgloss.Style
|
||||||
|
StatusBarFilterCount lipgloss.Style
|
||||||
|
|
||||||
|
NoItems lipgloss.Style
|
||||||
|
|
||||||
|
PaginationStyle lipgloss.Style
|
||||||
|
HelpStyle lipgloss.Style
|
||||||
|
|
||||||
|
// Styled characters.
|
||||||
|
ActivePaginationDot lipgloss.Style
|
||||||
|
InactivePaginationDot lipgloss.Style
|
||||||
|
ArabicPagination lipgloss.Style
|
||||||
|
DividerDot lipgloss.Style
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultStyles returns a set of default style definitions for this list
|
||||||
|
// component.
|
||||||
|
func DefaultStyles() (s Styles) {
|
||||||
|
verySubduedColor := lipgloss.AdaptiveColor{Light: "#DDDADA", Dark: "#3C3C3C"}
|
||||||
|
subduedColor := lipgloss.AdaptiveColor{Light: "#9B9B9B", Dark: "#5C5C5C"}
|
||||||
|
|
||||||
|
s.TitleBar = lipgloss.NewStyle().Padding(0, 0, 1, 2) //nolint:mnd
|
||||||
|
|
||||||
|
s.Title = lipgloss.NewStyle().
|
||||||
|
Background(lipgloss.Color("62")).
|
||||||
|
Foreground(lipgloss.Color("230")).
|
||||||
|
Padding(0, 1)
|
||||||
|
|
||||||
|
s.Spinner = lipgloss.NewStyle().
|
||||||
|
Foreground(lipgloss.AdaptiveColor{Light: "#8E8E8E", Dark: "#747373"})
|
||||||
|
|
||||||
|
s.FilterPrompt = lipgloss.NewStyle().
|
||||||
|
Foreground(lipgloss.AdaptiveColor{Light: "#04B575", Dark: "#ECFD65"})
|
||||||
|
|
||||||
|
s.FilterCursor = lipgloss.NewStyle().
|
||||||
|
Foreground(lipgloss.AdaptiveColor{Light: "#EE6FF8", Dark: "#EE6FF8"})
|
||||||
|
|
||||||
|
s.DefaultFilterCharacterMatch = lipgloss.NewStyle().Underline(true)
|
||||||
|
|
||||||
|
s.StatusBar = lipgloss.NewStyle().
|
||||||
|
Foreground(lipgloss.AdaptiveColor{Light: "#A49FA5", Dark: "#777777"}).
|
||||||
|
Padding(0, 0, 1, 2) //nolint:mnd
|
||||||
|
|
||||||
|
s.StatusEmpty = lipgloss.NewStyle().Foreground(subduedColor)
|
||||||
|
|
||||||
|
s.StatusBarActiveFilter = lipgloss.NewStyle().
|
||||||
|
Foreground(lipgloss.AdaptiveColor{Light: "#1a1a1a", Dark: "#dddddd"})
|
||||||
|
|
||||||
|
s.StatusBarFilterCount = lipgloss.NewStyle().Foreground(verySubduedColor)
|
||||||
|
|
||||||
|
s.NoItems = lipgloss.NewStyle().
|
||||||
|
Foreground(lipgloss.AdaptiveColor{Light: "#909090", Dark: "#626262"})
|
||||||
|
|
||||||
|
s.ArabicPagination = lipgloss.NewStyle().Foreground(subduedColor)
|
||||||
|
|
||||||
|
s.PaginationStyle = lipgloss.NewStyle().PaddingLeft(2) //nolint:mnd
|
||||||
|
|
||||||
|
s.HelpStyle = lipgloss.NewStyle().Padding(1, 0, 0, 2) //nolint:mnd
|
||||||
|
|
||||||
|
s.ActivePaginationDot = lipgloss.NewStyle().
|
||||||
|
Foreground(lipgloss.AdaptiveColor{Light: "#847A85", Dark: "#979797"}).
|
||||||
|
SetString(bullet)
|
||||||
|
|
||||||
|
s.InactivePaginationDot = lipgloss.NewStyle().
|
||||||
|
Foreground(verySubduedColor).
|
||||||
|
SetString(bullet)
|
||||||
|
|
||||||
|
s.DividerDot = lipgloss.NewStyle().
|
||||||
|
Foreground(verySubduedColor).
|
||||||
|
SetString(" " + bullet + " ")
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
214
vendor/github.com/charmbracelet/bubbles/paginator/paginator.go
generated
vendored
Normal file
214
vendor/github.com/charmbracelet/bubbles/paginator/paginator.go
generated
vendored
Normal file
@ -0,0 +1,214 @@
|
|||||||
|
// Package paginator provides a Bubble Tea package for calculating pagination
|
||||||
|
// and rendering pagination info. Note that this package does not render actual
|
||||||
|
// pages: it's purely for handling keystrokes related to pagination, and
|
||||||
|
// rendering pagination status.
|
||||||
|
package paginator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/bubbles/key"
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Type specifies the way we render pagination.
|
||||||
|
type Type int
|
||||||
|
|
||||||
|
// Pagination rendering options.
|
||||||
|
const (
|
||||||
|
Arabic Type = iota
|
||||||
|
Dots
|
||||||
|
)
|
||||||
|
|
||||||
|
// KeyMap is the key bindings for different actions within the paginator.
|
||||||
|
type KeyMap struct {
|
||||||
|
PrevPage key.Binding
|
||||||
|
NextPage key.Binding
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultKeyMap is the default set of key bindings for navigating and acting
|
||||||
|
// upon the paginator.
|
||||||
|
var DefaultKeyMap = KeyMap{
|
||||||
|
PrevPage: key.NewBinding(key.WithKeys("pgup", "left", "h")),
|
||||||
|
NextPage: key.NewBinding(key.WithKeys("pgdown", "right", "l")),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Model is the Bubble Tea model for this user interface.
|
||||||
|
type Model struct {
|
||||||
|
// Type configures how the pagination is rendered (Arabic, Dots).
|
||||||
|
Type Type
|
||||||
|
// Page is the current page number.
|
||||||
|
Page int
|
||||||
|
// PerPage is the number of items per page.
|
||||||
|
PerPage int
|
||||||
|
// TotalPages is the total number of pages.
|
||||||
|
TotalPages int
|
||||||
|
// ActiveDot is used to mark the current page under the Dots display type.
|
||||||
|
ActiveDot string
|
||||||
|
// InactiveDot is used to mark inactive pages under the Dots display type.
|
||||||
|
InactiveDot string
|
||||||
|
// ArabicFormat is the printf-style format to use for the Arabic display type.
|
||||||
|
ArabicFormat string
|
||||||
|
|
||||||
|
// KeyMap encodes the keybindings recognized by the widget.
|
||||||
|
KeyMap KeyMap
|
||||||
|
|
||||||
|
// Deprecated: customize [KeyMap] instead.
|
||||||
|
UsePgUpPgDownKeys bool
|
||||||
|
// Deprecated: customize [KeyMap] instead.
|
||||||
|
UseLeftRightKeys bool
|
||||||
|
// Deprecated: customize [KeyMap] instead.
|
||||||
|
UseUpDownKeys bool
|
||||||
|
// Deprecated: customize [KeyMap] instead.
|
||||||
|
UseHLKeys bool
|
||||||
|
// Deprecated: customize [KeyMap] instead.
|
||||||
|
UseJKKeys bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTotalPages is a helper function for calculating the total number of pages
|
||||||
|
// from a given number of items. Its use is optional since this pager can be
|
||||||
|
// used for other things beyond navigating sets. Note that it both returns the
|
||||||
|
// number of total pages and alters the model.
|
||||||
|
func (m *Model) SetTotalPages(items int) int {
|
||||||
|
if items < 1 {
|
||||||
|
return m.TotalPages
|
||||||
|
}
|
||||||
|
n := items / m.PerPage
|
||||||
|
if items%m.PerPage > 0 {
|
||||||
|
n++
|
||||||
|
}
|
||||||
|
m.TotalPages = n
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
// ItemsOnPage is a helper function for returning the number of items on the
|
||||||
|
// current page given the total number of items passed as an argument.
|
||||||
|
func (m Model) ItemsOnPage(totalItems int) int {
|
||||||
|
if totalItems < 1 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
start, end := m.GetSliceBounds(totalItems)
|
||||||
|
return end - start
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSliceBounds is a helper function for paginating slices. Pass the length
|
||||||
|
// of the slice you're rendering and you'll receive the start and end bounds
|
||||||
|
// corresponding to the pagination. For example:
|
||||||
|
//
|
||||||
|
// bunchOfStuff := []stuff{...}
|
||||||
|
// start, end := model.GetSliceBounds(len(bunchOfStuff))
|
||||||
|
// sliceToRender := bunchOfStuff[start:end]
|
||||||
|
func (m *Model) GetSliceBounds(length int) (start int, end int) {
|
||||||
|
start = m.Page * m.PerPage
|
||||||
|
end = min(m.Page*m.PerPage+m.PerPage, length)
|
||||||
|
return start, end
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrevPage is a helper function for navigating one page backward. It will not
|
||||||
|
// page beyond the first page (i.e. page 0).
|
||||||
|
func (m *Model) PrevPage() {
|
||||||
|
if m.Page > 0 {
|
||||||
|
m.Page--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NextPage is a helper function for navigating one page forward. It will not
|
||||||
|
// page beyond the last page (i.e. totalPages - 1).
|
||||||
|
func (m *Model) NextPage() {
|
||||||
|
if !m.OnLastPage() {
|
||||||
|
m.Page++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnLastPage returns whether or not we're on the last page.
|
||||||
|
func (m Model) OnLastPage() bool {
|
||||||
|
return m.Page == m.TotalPages-1
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnFirstPage returns whether or not we're on the first page.
|
||||||
|
func (m Model) OnFirstPage() bool {
|
||||||
|
return m.Page == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Option is used to set options in New.
|
||||||
|
type Option func(*Model)
|
||||||
|
|
||||||
|
// New creates a new model with defaults.
|
||||||
|
func New(opts ...Option) Model {
|
||||||
|
m := Model{
|
||||||
|
Type: Arabic,
|
||||||
|
Page: 0,
|
||||||
|
PerPage: 1,
|
||||||
|
TotalPages: 1,
|
||||||
|
KeyMap: DefaultKeyMap,
|
||||||
|
ActiveDot: "•",
|
||||||
|
InactiveDot: "○",
|
||||||
|
ArabicFormat: "%d/%d",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(&m)
|
||||||
|
}
|
||||||
|
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewModel creates a new model with defaults.
|
||||||
|
//
|
||||||
|
// Deprecated: use [New] instead.
|
||||||
|
var NewModel = New
|
||||||
|
|
||||||
|
// WithTotalPages sets the total pages.
|
||||||
|
func WithTotalPages(totalPages int) Option {
|
||||||
|
return func(m *Model) {
|
||||||
|
m.TotalPages = totalPages
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithPerPage sets the total pages.
|
||||||
|
func WithPerPage(perPage int) Option {
|
||||||
|
return func(m *Model) {
|
||||||
|
m.PerPage = perPage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update is the Tea update function which binds keystrokes to pagination.
|
||||||
|
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
|
||||||
|
switch msg := msg.(type) {
|
||||||
|
case tea.KeyMsg:
|
||||||
|
switch {
|
||||||
|
case key.Matches(msg, m.KeyMap.NextPage):
|
||||||
|
m.NextPage()
|
||||||
|
case key.Matches(msg, m.KeyMap.PrevPage):
|
||||||
|
m.PrevPage()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// View renders the pagination to a string.
|
||||||
|
func (m Model) View() string {
|
||||||
|
switch m.Type { //nolint:exhaustive
|
||||||
|
case Dots:
|
||||||
|
return m.dotsView()
|
||||||
|
default:
|
||||||
|
return m.arabicView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Model) dotsView() string {
|
||||||
|
var s string
|
||||||
|
for i := 0; i < m.TotalPages; i++ {
|
||||||
|
if i == m.Page {
|
||||||
|
s += m.ActiveDot
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
s += m.InactiveDot
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Model) arabicView() string {
|
||||||
|
return fmt.Sprintf(m.ArabicFormat, m.Page+1, m.TotalPages)
|
||||||
|
}
|
||||||
439
vendor/github.com/evertras/bubble-table/table/border.go
generated
vendored
439
vendor/github.com/evertras/bubble-table/table/border.go
generated
vendored
@ -1,439 +0,0 @@
|
|||||||
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
36
vendor/github.com/evertras/bubble-table/table/calc.go
generated
vendored
@ -1,36 +0,0 @@
|
|||||||
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
60
vendor/github.com/evertras/bubble-table/table/cell.go
generated
vendored
@ -1,60 +0,0 @@
|
|||||||
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
118
vendor/github.com/evertras/bubble-table/table/column.go
generated
vendored
@ -1,118 +0,0 @@
|
|||||||
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
67
vendor/github.com/evertras/bubble-table/table/data.go
generated
vendored
@ -1,67 +0,0 @@
|
|||||||
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
116
vendor/github.com/evertras/bubble-table/table/dimensions.go
generated
vendored
@ -1,116 +0,0 @@
|
|||||||
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
39
vendor/github.com/evertras/bubble-table/table/doc.go
generated
vendored
@ -1,39 +0,0 @@
|
|||||||
/*
|
|
||||||
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
60
vendor/github.com/evertras/bubble-table/table/events.go
generated
vendored
@ -1,60 +0,0 @@
|
|||||||
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
164
vendor/github.com/evertras/bubble-table/table/filter.go
generated
vendored
@ -1,164 +0,0 @@
|
|||||||
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
51
vendor/github.com/evertras/bubble-table/table/footer.go
generated
vendored
@ -1,51 +0,0 @@
|
|||||||
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
93
vendor/github.com/evertras/bubble-table/table/header.go
generated
vendored
@ -1,93 +0,0 @@
|
|||||||
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
120
vendor/github.com/evertras/bubble-table/table/keys.go
generated
vendored
@ -1,120 +0,0 @@
|
|||||||
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
148
vendor/github.com/evertras/bubble-table/table/model.go
generated
vendored
@ -1,148 +0,0 @@
|
|||||||
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
510
vendor/github.com/evertras/bubble-table/table/options.go
generated
vendored
@ -1,510 +0,0 @@
|
|||||||
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
18
vendor/github.com/evertras/bubble-table/table/overflow.go
generated
vendored
@ -1,18 +0,0 @@
|
|||||||
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
112
vendor/github.com/evertras/bubble-table/table/pagination.go
generated
vendored
@ -1,112 +0,0 @@
|
|||||||
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
96
vendor/github.com/evertras/bubble-table/table/query.go
generated
vendored
@ -1,96 +0,0 @@
|
|||||||
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
252
vendor/github.com/evertras/bubble-table/table/row.go
generated
vendored
@ -1,252 +0,0 @@
|
|||||||
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
50
vendor/github.com/evertras/bubble-table/table/scrolling.go
generated
vendored
@ -1,50 +0,0 @@
|
|||||||
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
178
vendor/github.com/evertras/bubble-table/table/sort.go
generated
vendored
@ -1,178 +0,0 @@
|
|||||||
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
26
vendor/github.com/evertras/bubble-table/table/strlimit.go
generated
vendored
@ -1,26 +0,0 @@
|
|||||||
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
154
vendor/github.com/evertras/bubble-table/table/update.go
generated
vendored
@ -1,154 +0,0 @@
|
|||||||
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
65
vendor/github.com/evertras/bubble-table/table/view.go
generated
vendored
@ -1,65 +0,0 @@
|
|||||||
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()
|
|
||||||
}
|
|
||||||
21
vendor/github.com/muesli/reflow/LICENSE
generated
vendored
21
vendor/github.com/muesli/reflow/LICENSE
generated
vendored
@ -1,21 +0,0 @@
|
|||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2019 Christian Muehlhaeuser
|
|
||||||
|
|
||||||
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.
|
|
||||||
7
vendor/github.com/muesli/reflow/ansi/ansi.go
generated
vendored
7
vendor/github.com/muesli/reflow/ansi/ansi.go
generated
vendored
@ -1,7 +0,0 @@
|
|||||||
package ansi
|
|
||||||
|
|
||||||
const Marker = '\x1B'
|
|
||||||
|
|
||||||
func IsTerminator(c rune) bool {
|
|
||||||
return (c >= 0x40 && c <= 0x5a) || (c >= 0x61 && c <= 0x7a)
|
|
||||||
}
|
|
||||||
40
vendor/github.com/muesli/reflow/ansi/buffer.go
generated
vendored
40
vendor/github.com/muesli/reflow/ansi/buffer.go
generated
vendored
@ -1,40 +0,0 @@
|
|||||||
package ansi
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
|
|
||||||
"github.com/mattn/go-runewidth"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Buffer is a buffer aware of ANSI escape sequences.
|
|
||||||
type Buffer struct {
|
|
||||||
bytes.Buffer
|
|
||||||
}
|
|
||||||
|
|
||||||
// PrintableRuneWidth returns the cell width of all printable runes in the
|
|
||||||
// buffer.
|
|
||||||
func (w Buffer) PrintableRuneWidth() int {
|
|
||||||
return PrintableRuneWidth(w.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
// PrintableRuneWidth returns the cell width of the given string.
|
|
||||||
func PrintableRuneWidth(s string) int {
|
|
||||||
var n int
|
|
||||||
var ansi bool
|
|
||||||
|
|
||||||
for _, c := range s {
|
|
||||||
if c == Marker {
|
|
||||||
// ANSI escape sequence
|
|
||||||
ansi = true
|
|
||||||
} else if ansi {
|
|
||||||
if IsTerminator(c) {
|
|
||||||
// ANSI sequence terminated
|
|
||||||
ansi = false
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
n += runewidth.RuneWidth(c)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return n
|
|
||||||
}
|
|
||||||
76
vendor/github.com/muesli/reflow/ansi/writer.go
generated
vendored
76
vendor/github.com/muesli/reflow/ansi/writer.go
generated
vendored
@ -1,76 +0,0 @@
|
|||||||
package ansi
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"io"
|
|
||||||
"unicode/utf8"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Writer struct {
|
|
||||||
Forward io.Writer
|
|
||||||
|
|
||||||
ansi bool
|
|
||||||
ansiseq bytes.Buffer
|
|
||||||
lastseq bytes.Buffer
|
|
||||||
seqchanged bool
|
|
||||||
runeBuf []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write is used to write content to the ANSI buffer.
|
|
||||||
func (w *Writer) Write(b []byte) (int, error) {
|
|
||||||
for _, c := range string(b) {
|
|
||||||
if c == Marker {
|
|
||||||
// ANSI escape sequence
|
|
||||||
w.ansi = true
|
|
||||||
w.seqchanged = true
|
|
||||||
_, _ = w.ansiseq.WriteRune(c)
|
|
||||||
} else if w.ansi {
|
|
||||||
_, _ = w.ansiseq.WriteRune(c)
|
|
||||||
if IsTerminator(c) {
|
|
||||||
// ANSI sequence terminated
|
|
||||||
w.ansi = false
|
|
||||||
|
|
||||||
if bytes.HasSuffix(w.ansiseq.Bytes(), []byte("[0m")) {
|
|
||||||
// reset sequence
|
|
||||||
w.lastseq.Reset()
|
|
||||||
w.seqchanged = false
|
|
||||||
} else if c == 'm' {
|
|
||||||
// color code
|
|
||||||
_, _ = w.lastseq.Write(w.ansiseq.Bytes())
|
|
||||||
}
|
|
||||||
|
|
||||||
_, _ = w.ansiseq.WriteTo(w.Forward)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
_, err := w.writeRune(c)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return len(b), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *Writer) writeRune(r rune) (int, error) {
|
|
||||||
if w.runeBuf == nil {
|
|
||||||
w.runeBuf = make([]byte, utf8.UTFMax)
|
|
||||||
}
|
|
||||||
n := utf8.EncodeRune(w.runeBuf, r)
|
|
||||||
return w.Forward.Write(w.runeBuf[:n])
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *Writer) LastSequence() string {
|
|
||||||
return w.lastseq.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *Writer) ResetAnsi() {
|
|
||||||
if !w.seqchanged {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
_, _ = w.Forward.Write([]byte("\x1b[0m"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *Writer) RestoreAnsi() {
|
|
||||||
_, _ = w.Forward.Write(w.lastseq.Bytes())
|
|
||||||
}
|
|
||||||
120
vendor/github.com/muesli/reflow/truncate/truncate.go
generated
vendored
120
vendor/github.com/muesli/reflow/truncate/truncate.go
generated
vendored
@ -1,120 +0,0 @@
|
|||||||
package truncate
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"io"
|
|
||||||
|
|
||||||
"github.com/mattn/go-runewidth"
|
|
||||||
|
|
||||||
"github.com/muesli/reflow/ansi"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Writer struct {
|
|
||||||
width uint
|
|
||||||
tail string
|
|
||||||
|
|
||||||
ansiWriter *ansi.Writer
|
|
||||||
buf bytes.Buffer
|
|
||||||
ansi bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewWriter(width uint, tail string) *Writer {
|
|
||||||
w := &Writer{
|
|
||||||
width: width,
|
|
||||||
tail: tail,
|
|
||||||
}
|
|
||||||
w.ansiWriter = &ansi.Writer{
|
|
||||||
Forward: &w.buf,
|
|
||||||
}
|
|
||||||
return w
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewWriterPipe(forward io.Writer, width uint, tail string) *Writer {
|
|
||||||
return &Writer{
|
|
||||||
width: width,
|
|
||||||
tail: tail,
|
|
||||||
ansiWriter: &ansi.Writer{
|
|
||||||
Forward: forward,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bytes is shorthand for declaring a new default truncate-writer instance,
|
|
||||||
// used to immediately truncate a byte slice.
|
|
||||||
func Bytes(b []byte, width uint) []byte {
|
|
||||||
return BytesWithTail(b, width, []byte(""))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bytes is shorthand for declaring a new default truncate-writer instance,
|
|
||||||
// used to immediately truncate a byte slice. A tail is then added to the
|
|
||||||
// end of the byte slice.
|
|
||||||
func BytesWithTail(b []byte, width uint, tail []byte) []byte {
|
|
||||||
f := NewWriter(width, string(tail))
|
|
||||||
_, _ = f.Write(b)
|
|
||||||
|
|
||||||
return f.Bytes()
|
|
||||||
}
|
|
||||||
|
|
||||||
// String is shorthand for declaring a new default truncate-writer instance,
|
|
||||||
// used to immediately truncate a string.
|
|
||||||
func String(s string, width uint) string {
|
|
||||||
return StringWithTail(s, width, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
// StringWithTail is shorthand for declaring a new default truncate-writer instance,
|
|
||||||
// used to immediately truncate a string. A tail is then added to the end of the
|
|
||||||
// string.
|
|
||||||
func StringWithTail(s string, width uint, tail string) string {
|
|
||||||
return string(BytesWithTail([]byte(s), width, []byte(tail)))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write truncates content at the given printable cell width, leaving any
|
|
||||||
// ansi sequences intact.
|
|
||||||
func (w *Writer) Write(b []byte) (int, error) {
|
|
||||||
tw := ansi.PrintableRuneWidth(w.tail)
|
|
||||||
if w.width < uint(tw) {
|
|
||||||
return w.buf.WriteString(w.tail)
|
|
||||||
}
|
|
||||||
|
|
||||||
w.width -= uint(tw)
|
|
||||||
var curWidth uint
|
|
||||||
|
|
||||||
for _, c := range string(b) {
|
|
||||||
if c == ansi.Marker {
|
|
||||||
// ANSI escape sequence
|
|
||||||
w.ansi = true
|
|
||||||
} else if w.ansi {
|
|
||||||
if ansi.IsTerminator(c) {
|
|
||||||
// ANSI sequence terminated
|
|
||||||
w.ansi = false
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
curWidth += uint(runewidth.RuneWidth(c))
|
|
||||||
}
|
|
||||||
|
|
||||||
if curWidth > w.width {
|
|
||||||
n, err := w.buf.WriteString(w.tail)
|
|
||||||
if w.ansiWriter.LastSequence() != "" {
|
|
||||||
w.ansiWriter.ResetAnsi()
|
|
||||||
}
|
|
||||||
return n, err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := w.ansiWriter.Write([]byte(string(c)))
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return len(b), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bytes returns the truncated result as a byte slice.
|
|
||||||
func (w *Writer) Bytes() []byte {
|
|
||||||
return w.buf.Bytes()
|
|
||||||
}
|
|
||||||
|
|
||||||
// String returns the truncated result as a string.
|
|
||||||
func (w *Writer) String() string {
|
|
||||||
return w.buf.String()
|
|
||||||
}
|
|
||||||
167
vendor/github.com/muesli/reflow/wordwrap/wordwrap.go
generated
vendored
167
vendor/github.com/muesli/reflow/wordwrap/wordwrap.go
generated
vendored
@ -1,167 +0,0 @@
|
|||||||
package wordwrap
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"strings"
|
|
||||||
"unicode"
|
|
||||||
|
|
||||||
"github.com/muesli/reflow/ansi"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
defaultBreakpoints = []rune{'-'}
|
|
||||||
defaultNewline = []rune{'\n'}
|
|
||||||
)
|
|
||||||
|
|
||||||
// WordWrap contains settings and state for customisable text reflowing with
|
|
||||||
// support for ANSI escape sequences. This means you can style your terminal
|
|
||||||
// output without affecting the word wrapping algorithm.
|
|
||||||
type WordWrap struct {
|
|
||||||
Limit int
|
|
||||||
Breakpoints []rune
|
|
||||||
Newline []rune
|
|
||||||
KeepNewlines bool
|
|
||||||
|
|
||||||
buf bytes.Buffer
|
|
||||||
space bytes.Buffer
|
|
||||||
word ansi.Buffer
|
|
||||||
|
|
||||||
lineLen int
|
|
||||||
ansi bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewWriter returns a new instance of a word-wrapping writer, initialized with
|
|
||||||
// default settings.
|
|
||||||
func NewWriter(limit int) *WordWrap {
|
|
||||||
return &WordWrap{
|
|
||||||
Limit: limit,
|
|
||||||
Breakpoints: defaultBreakpoints,
|
|
||||||
Newline: defaultNewline,
|
|
||||||
KeepNewlines: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bytes is shorthand for declaring a new default WordWrap instance,
|
|
||||||
// used to immediately word-wrap a byte slice.
|
|
||||||
func Bytes(b []byte, limit int) []byte {
|
|
||||||
f := NewWriter(limit)
|
|
||||||
_, _ = f.Write(b)
|
|
||||||
_ = f.Close()
|
|
||||||
|
|
||||||
return f.Bytes()
|
|
||||||
}
|
|
||||||
|
|
||||||
// String is shorthand for declaring a new default WordWrap instance,
|
|
||||||
// used to immediately word-wrap a string.
|
|
||||||
func String(s string, limit int) string {
|
|
||||||
return string(Bytes([]byte(s), limit))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *WordWrap) addSpace() {
|
|
||||||
w.lineLen += w.space.Len()
|
|
||||||
_, _ = w.buf.Write(w.space.Bytes())
|
|
||||||
w.space.Reset()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *WordWrap) addWord() {
|
|
||||||
if w.word.Len() > 0 {
|
|
||||||
w.addSpace()
|
|
||||||
w.lineLen += w.word.PrintableRuneWidth()
|
|
||||||
_, _ = w.buf.Write(w.word.Bytes())
|
|
||||||
w.word.Reset()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *WordWrap) addNewLine() {
|
|
||||||
_, _ = w.buf.WriteRune('\n')
|
|
||||||
w.lineLen = 0
|
|
||||||
w.space.Reset()
|
|
||||||
}
|
|
||||||
|
|
||||||
func inGroup(a []rune, c rune) bool {
|
|
||||||
for _, v := range a {
|
|
||||||
if v == c {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write is used to write more content to the word-wrap buffer.
|
|
||||||
func (w *WordWrap) Write(b []byte) (int, error) {
|
|
||||||
if w.Limit == 0 {
|
|
||||||
return w.buf.Write(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
s := string(b)
|
|
||||||
if !w.KeepNewlines {
|
|
||||||
s = strings.Replace(strings.TrimSpace(s), "\n", " ", -1)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, c := range s {
|
|
||||||
if c == '\x1B' {
|
|
||||||
// ANSI escape sequence
|
|
||||||
_, _ = w.word.WriteRune(c)
|
|
||||||
w.ansi = true
|
|
||||||
} else if w.ansi {
|
|
||||||
_, _ = w.word.WriteRune(c)
|
|
||||||
if (c >= 0x40 && c <= 0x5a) || (c >= 0x61 && c <= 0x7a) {
|
|
||||||
// ANSI sequence terminated
|
|
||||||
w.ansi = false
|
|
||||||
}
|
|
||||||
} else if inGroup(w.Newline, c) {
|
|
||||||
// end of current line
|
|
||||||
// see if we can add the content of the space buffer to the current line
|
|
||||||
if w.word.Len() == 0 {
|
|
||||||
if w.lineLen+w.space.Len() > w.Limit {
|
|
||||||
w.lineLen = 0
|
|
||||||
} else {
|
|
||||||
// preserve whitespace
|
|
||||||
_, _ = w.buf.Write(w.space.Bytes())
|
|
||||||
}
|
|
||||||
w.space.Reset()
|
|
||||||
}
|
|
||||||
|
|
||||||
w.addWord()
|
|
||||||
w.addNewLine()
|
|
||||||
} else if unicode.IsSpace(c) {
|
|
||||||
// end of current word
|
|
||||||
w.addWord()
|
|
||||||
_, _ = w.space.WriteRune(c)
|
|
||||||
} else if inGroup(w.Breakpoints, c) {
|
|
||||||
// valid breakpoint
|
|
||||||
w.addSpace()
|
|
||||||
w.addWord()
|
|
||||||
_, _ = w.buf.WriteRune(c)
|
|
||||||
} else {
|
|
||||||
// any other character
|
|
||||||
_, _ = w.word.WriteRune(c)
|
|
||||||
|
|
||||||
// add a line break if the current word would exceed the line's
|
|
||||||
// character limit
|
|
||||||
if w.lineLen+w.space.Len()+w.word.PrintableRuneWidth() > w.Limit &&
|
|
||||||
w.word.PrintableRuneWidth() < w.Limit {
|
|
||||||
w.addNewLine()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return len(b), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close will finish the word-wrap operation. Always call it before trying to
|
|
||||||
// retrieve the final result.
|
|
||||||
func (w *WordWrap) Close() error {
|
|
||||||
w.addWord()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bytes returns the word-wrapped result as a byte slice.
|
|
||||||
func (w *WordWrap) Bytes() []byte {
|
|
||||||
return w.buf.Bytes()
|
|
||||||
}
|
|
||||||
|
|
||||||
// String returns the word-wrapped result as a string.
|
|
||||||
func (w *WordWrap) String() string {
|
|
||||||
return w.buf.String()
|
|
||||||
}
|
|
||||||
18
vendor/github.com/sahilm/fuzzy/.editorconfig
generated
vendored
Normal file
18
vendor/github.com/sahilm/fuzzy/.editorconfig
generated
vendored
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[*.sh]
|
||||||
|
indent_size = 4
|
||||||
|
|
||||||
|
[Makefile]
|
||||||
|
indent_style = tab
|
||||||
|
indent_size = 4
|
||||||
|
|
||||||
|
[*.go]
|
||||||
|
indent_style = tab
|
||||||
|
indent_size = 4
|
||||||
2
vendor/github.com/sahilm/fuzzy/.gitignore
generated
vendored
Normal file
2
vendor/github.com/sahilm/fuzzy/.gitignore
generated
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
vendor/
|
||||||
|
coverage/
|
||||||
8
vendor/github.com/sahilm/fuzzy/.travis.yml
generated
vendored
Normal file
8
vendor/github.com/sahilm/fuzzy/.travis.yml
generated
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
arch:
|
||||||
|
- amd64
|
||||||
|
- ppc64le
|
||||||
|
language: go
|
||||||
|
go:
|
||||||
|
- 1.x
|
||||||
|
script:
|
||||||
|
- make
|
||||||
1
vendor/github.com/sahilm/fuzzy/CONTRIBUTING.md
generated
vendored
Normal file
1
vendor/github.com/sahilm/fuzzy/CONTRIBUTING.md
generated
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
Everyone is welcome to contribute. Please send me a pull request or file an issue. I promise to respond promptly.
|
||||||
20
vendor/github.com/sahilm/fuzzy/Gopkg.lock
generated
vendored
Normal file
20
vendor/github.com/sahilm/fuzzy/Gopkg.lock
generated
vendored
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
|
||||||
|
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
branch = "master"
|
||||||
|
digest = "1:ee97ec8a00b2424570c1ce53d7b410e96fbd4c241b29df134276ff6aa3750335"
|
||||||
|
name = "github.com/kylelemons/godebug"
|
||||||
|
packages = [
|
||||||
|
"diff",
|
||||||
|
"pretty",
|
||||||
|
]
|
||||||
|
pruneopts = ""
|
||||||
|
revision = "d65d576e9348f5982d7f6d83682b694e731a45c6"
|
||||||
|
|
||||||
|
[solve-meta]
|
||||||
|
analyzer-name = "dep"
|
||||||
|
analyzer-version = 1
|
||||||
|
input-imports = ["github.com/kylelemons/godebug/pretty"]
|
||||||
|
solver-name = "gps-cdcl"
|
||||||
|
solver-version = 1
|
||||||
4
vendor/github.com/sahilm/fuzzy/Gopkg.toml
generated
vendored
Normal file
4
vendor/github.com/sahilm/fuzzy/Gopkg.toml
generated
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# Test dependency
|
||||||
|
[[constraint]]
|
||||||
|
branch = "master"
|
||||||
|
name = "github.com/kylelemons/godebug"
|
||||||
@ -1,6 +1,6 @@
|
|||||||
MIT License
|
The MIT License (MIT)
|
||||||
|
|
||||||
Copyright (c) 2022 Brandon Fulljames
|
Copyright (c) 2017 Sahil Muthoo
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
57
vendor/github.com/sahilm/fuzzy/Makefile
generated
vendored
Normal file
57
vendor/github.com/sahilm/fuzzy/Makefile
generated
vendored
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
.PHONY: all
|
||||||
|
all: setup lint test
|
||||||
|
|
||||||
|
.PHONY: test
|
||||||
|
test: setup
|
||||||
|
go test -bench ./...
|
||||||
|
|
||||||
|
.PHONY: cover
|
||||||
|
cover: setup
|
||||||
|
mkdir -p coverage
|
||||||
|
gocov test ./... | gocov-html > coverage/coverage.html
|
||||||
|
|
||||||
|
sources = $(shell find . -name '*.go' -not -path './vendor/*')
|
||||||
|
.PHONY: goimports
|
||||||
|
goimports: setup
|
||||||
|
goimports -w $(sources)
|
||||||
|
|
||||||
|
.PHONY: lint
|
||||||
|
lint: setup
|
||||||
|
gometalinter ./... --enable=goimports --disable=gocyclo --vendor -t
|
||||||
|
|
||||||
|
.PHONY: install
|
||||||
|
install: setup
|
||||||
|
go install
|
||||||
|
|
||||||
|
BIN_DIR := $(GOPATH)/bin
|
||||||
|
GOIMPORTS := $(BIN_DIR)/goimports
|
||||||
|
GOMETALINTER := $(BIN_DIR)/gometalinter
|
||||||
|
DEP := $(BIN_DIR)/dep
|
||||||
|
GOCOV := $(BIN_DIR)/gocov
|
||||||
|
GOCOV_HTML := $(BIN_DIR)/gocov-html
|
||||||
|
|
||||||
|
$(GOIMPORTS):
|
||||||
|
go get -u golang.org/x/tools/cmd/goimports
|
||||||
|
|
||||||
|
$(GOMETALINTER):
|
||||||
|
go get -u github.com/alecthomas/gometalinter
|
||||||
|
gometalinter --install &> /dev/null
|
||||||
|
|
||||||
|
$(GOCOV):
|
||||||
|
go get -u github.com/axw/gocov/gocov
|
||||||
|
|
||||||
|
$(GOCOV_HTML):
|
||||||
|
go get -u gopkg.in/matm/v1/gocov-html
|
||||||
|
|
||||||
|
$(DEP):
|
||||||
|
go get -u github.com/golang/dep/cmd/dep
|
||||||
|
|
||||||
|
tools: $(GOIMPORTS) $(GOMETALINTER) $(GOCOV) $(GOCOV_HTML) $(DEP)
|
||||||
|
|
||||||
|
vendor: $(DEP)
|
||||||
|
dep ensure
|
||||||
|
|
||||||
|
setup: tools vendor
|
||||||
|
|
||||||
|
updatedeps:
|
||||||
|
dep ensure -update
|
||||||
186
vendor/github.com/sahilm/fuzzy/README.md
generated
vendored
Normal file
186
vendor/github.com/sahilm/fuzzy/README.md
generated
vendored
Normal file
@ -0,0 +1,186 @@
|
|||||||
|
<img src="assets/search-gopher-1.png" alt="gopher looking for stuff"> <img src="assets/search-gopher-2.png" alt="gopher found stuff">
|
||||||
|
|
||||||
|
# fuzzy
|
||||||
|
[](https://travis-ci.org/sahilm/fuzzy)
|
||||||
|
[](https://godoc.org/github.com/sahilm/fuzzy)
|
||||||
|
|
||||||
|
Go library that provides fuzzy string matching optimized for filenames and code symbols in the style of Sublime Text,
|
||||||
|
VSCode, IntelliJ IDEA et al. This library is external dependency-free. It only depends on the Go standard library.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Intuitive matching. Results are returned in descending order of match quality. Quality is determined by:
|
||||||
|
- The first character in the pattern matches the first character in the match string.
|
||||||
|
- The matched character is camel cased.
|
||||||
|
- The matched character follows a separator such as an underscore character.
|
||||||
|
- The matched character is adjacent to a previous match.
|
||||||
|
|
||||||
|
- Speed. Matches are returned in milliseconds. It's perfect for interactive search boxes.
|
||||||
|
|
||||||
|
- The positions of matches are returned. Allows you to highlight matching characters.
|
||||||
|
|
||||||
|
- Unicode aware.
|
||||||
|
|
||||||
|
## Demo
|
||||||
|
|
||||||
|
Here is a [demo](_example/main.go) of matching various patterns against ~16K files from the Unreal Engine 4 codebase.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
You can run the demo yourself like so:
|
||||||
|
|
||||||
|
```
|
||||||
|
cd _example/
|
||||||
|
go get github.com/jroimartin/gocui
|
||||||
|
go run main.go
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
The following example prints out matches with the matched chars in bold.
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/sahilm/fuzzy"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
const bold = "\033[1m%s\033[0m"
|
||||||
|
pattern := "mnr"
|
||||||
|
data := []string{"game.cpp", "moduleNameResolver.ts", "my name is_Ramsey"}
|
||||||
|
|
||||||
|
matches := fuzzy.Find(pattern, data)
|
||||||
|
|
||||||
|
for _, match := range matches {
|
||||||
|
for i := 0; i < len(match.Str); i++ {
|
||||||
|
if contains(i, match.MatchedIndexes) {
|
||||||
|
fmt.Print(fmt.Sprintf(bold, string(match.Str[i])))
|
||||||
|
} else {
|
||||||
|
fmt.Print(string(match.Str[i]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func contains(needle int, haystack []int) bool {
|
||||||
|
for _, i := range haystack {
|
||||||
|
if needle == i {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
If the data you want to match isn't a slice of strings, you can use `FindFrom` by implementing
|
||||||
|
the provided `Source` interface. Here's an example:
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/sahilm/fuzzy"
|
||||||
|
)
|
||||||
|
|
||||||
|
type employee struct {
|
||||||
|
name string
|
||||||
|
age int
|
||||||
|
}
|
||||||
|
|
||||||
|
type employees []employee
|
||||||
|
|
||||||
|
func (e employees) String(i int) string {
|
||||||
|
return e[i].name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e employees) Len() int {
|
||||||
|
return len(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
emps := employees{
|
||||||
|
{
|
||||||
|
name: "Alice",
|
||||||
|
age: 45,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Bob",
|
||||||
|
age: 35,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Allie",
|
||||||
|
age: 35,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
results := fuzzy.FindFrom("al", emps)
|
||||||
|
for _, r := range results {
|
||||||
|
fmt.Println(emps[r.Index])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Check out the [godoc](https://godoc.org/github.com/sahilm/fuzzy) for detailed documentation.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
`go get github.com/sahilm/fuzzy` or use your favorite dependency management tool.
|
||||||
|
|
||||||
|
## Speed
|
||||||
|
|
||||||
|
Here are a few benchmark results on a normal laptop.
|
||||||
|
|
||||||
|
```
|
||||||
|
BenchmarkFind/with_unreal_4_(~16K_files)-4 100 12915315 ns/op
|
||||||
|
BenchmarkFind/with_linux_kernel_(~60K_files)-4 50 30885038 ns/op
|
||||||
|
```
|
||||||
|
|
||||||
|
Matching a pattern against ~60K files from the Linux kernel takes about 30ms.
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Everyone is welcome to contribute. Please send me a pull request or file an issue. I promise
|
||||||
|
to respond promptly.
|
||||||
|
|
||||||
|
## Credits
|
||||||
|
|
||||||
|
* [@ericpauley](https://github.com/ericpauley) & [@lunixbochs](https://github.com/lunixbochs) contributed Unicode awareness and various performance optimisations.
|
||||||
|
|
||||||
|
* The algorithm is based of the awesome work of [forrestthewoods](https://github.com/forrestthewoods/lib_fts/blob/master/code/fts_fuzzy_match.js).
|
||||||
|
See [this](https://blog.forrestthewoods.com/reverse-engineering-sublime-text-s-fuzzy-match-4cffeed33fdb#.d05n81yjy)
|
||||||
|
blog post for details of the algorithm.
|
||||||
|
|
||||||
|
* The artwork is by my lovely wife Sanah. It's based on the Go Gopher.
|
||||||
|
|
||||||
|
* The Go gopher was designed by Renee French (http://reneefrench.blogspot.com/).
|
||||||
|
The design is licensed under the Creative Commons 3.0 Attributions license.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2017 Sahil Muthoo
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
254
vendor/github.com/sahilm/fuzzy/fuzzy.go
generated
vendored
Normal file
254
vendor/github.com/sahilm/fuzzy/fuzzy.go
generated
vendored
Normal file
@ -0,0 +1,254 @@
|
|||||||
|
/*
|
||||||
|
Package fuzzy provides fuzzy string matching optimized
|
||||||
|
for filenames and code symbols in the style of Sublime Text,
|
||||||
|
VSCode, IntelliJ IDEA et al.
|
||||||
|
*/
|
||||||
|
package fuzzy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sort"
|
||||||
|
"unicode"
|
||||||
|
"unicode/utf8"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Match represents a matched string.
|
||||||
|
type Match struct {
|
||||||
|
// The matched string.
|
||||||
|
Str string
|
||||||
|
// The index of the matched string in the supplied slice.
|
||||||
|
Index int
|
||||||
|
// The indexes of matched characters. Useful for highlighting matches.
|
||||||
|
MatchedIndexes []int
|
||||||
|
// Score used to rank matches
|
||||||
|
Score int
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
firstCharMatchBonus = 10
|
||||||
|
matchFollowingSeparatorBonus = 20
|
||||||
|
camelCaseMatchBonus = 20
|
||||||
|
adjacentMatchBonus = 5
|
||||||
|
unmatchedLeadingCharPenalty = -5
|
||||||
|
maxUnmatchedLeadingCharPenalty = -15
|
||||||
|
)
|
||||||
|
|
||||||
|
var separators = []rune("/-_ .\\")
|
||||||
|
|
||||||
|
// Matches is a slice of Match structs
|
||||||
|
type Matches []Match
|
||||||
|
|
||||||
|
func (a Matches) Len() int { return len(a) }
|
||||||
|
func (a Matches) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||||
|
func (a Matches) Less(i, j int) bool { return a[i].Score >= a[j].Score }
|
||||||
|
|
||||||
|
// Source represents an abstract source of a list of strings. Source must be iterable type such as a slice.
|
||||||
|
// The source will be iterated over till Len() with String(i) being called for each element where i is the
|
||||||
|
// index of the element. You can find a working example in the README.
|
||||||
|
type Source interface {
|
||||||
|
// The string to be matched at position i.
|
||||||
|
String(i int) string
|
||||||
|
// The length of the source. Typically is the length of the slice of things that you want to match.
|
||||||
|
Len() int
|
||||||
|
}
|
||||||
|
|
||||||
|
type stringSource []string
|
||||||
|
|
||||||
|
func (ss stringSource) String(i int) string {
|
||||||
|
return ss[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ss stringSource) Len() int { return len(ss) }
|
||||||
|
|
||||||
|
/*
|
||||||
|
Find looks up pattern in data and returns matches
|
||||||
|
in descending order of match quality. Match quality
|
||||||
|
is determined by a set of bonus and penalty rules.
|
||||||
|
|
||||||
|
The following types of matches apply a bonus:
|
||||||
|
|
||||||
|
* The first character in the pattern matches the first character in the match string.
|
||||||
|
|
||||||
|
* The matched character is camel cased.
|
||||||
|
|
||||||
|
* The matched character follows a separator such as an underscore character.
|
||||||
|
|
||||||
|
* The matched character is adjacent to a previous match.
|
||||||
|
|
||||||
|
Penalties are applied for every character in the search string that wasn't matched and all leading
|
||||||
|
characters upto the first match.
|
||||||
|
|
||||||
|
Results are sorted by best match.
|
||||||
|
*/
|
||||||
|
func Find(pattern string, data []string) Matches {
|
||||||
|
return FindFrom(pattern, stringSource(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
FindNoSort is an alternative Find implementation that does not sort
|
||||||
|
the results in the end.
|
||||||
|
*/
|
||||||
|
func FindNoSort(pattern string, data []string) Matches {
|
||||||
|
return FindFromNoSort(pattern, stringSource(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
FindFrom is an alternative implementation of Find using a Source
|
||||||
|
instead of a list of strings.
|
||||||
|
*/
|
||||||
|
func FindFrom(pattern string, data Source) Matches {
|
||||||
|
matches := FindFromNoSort(pattern, data)
|
||||||
|
sort.Stable(matches)
|
||||||
|
return matches
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
FindFromNoSort is an alternative FindFrom implementation that does
|
||||||
|
not sort results in the end.
|
||||||
|
*/
|
||||||
|
func FindFromNoSort(pattern string, data Source) Matches {
|
||||||
|
if len(pattern) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
runes := []rune(pattern)
|
||||||
|
var matches Matches
|
||||||
|
var matchedIndexes []int
|
||||||
|
for i := 0; i < data.Len(); i++ {
|
||||||
|
var match Match
|
||||||
|
match.Str = data.String(i)
|
||||||
|
match.Index = i
|
||||||
|
if matchedIndexes != nil {
|
||||||
|
match.MatchedIndexes = matchedIndexes
|
||||||
|
} else {
|
||||||
|
match.MatchedIndexes = make([]int, 0, len(runes))
|
||||||
|
}
|
||||||
|
var score int
|
||||||
|
patternIndex := 0
|
||||||
|
bestScore := -1
|
||||||
|
matchedIndex := -1
|
||||||
|
currAdjacentMatchBonus := 0
|
||||||
|
var last rune
|
||||||
|
var lastIndex int
|
||||||
|
nextc, nextSize := utf8.DecodeRuneInString(data.String(i))
|
||||||
|
var candidate rune
|
||||||
|
var candidateSize int
|
||||||
|
for j := 0; j < len(data.String(i)); j += candidateSize {
|
||||||
|
candidate, candidateSize = nextc, nextSize
|
||||||
|
if equalFold(candidate, runes[patternIndex]) {
|
||||||
|
score = 0
|
||||||
|
if j == 0 {
|
||||||
|
score += firstCharMatchBonus
|
||||||
|
}
|
||||||
|
if unicode.IsLower(last) && unicode.IsUpper(candidate) {
|
||||||
|
score += camelCaseMatchBonus
|
||||||
|
}
|
||||||
|
if j != 0 && isSeparator(last) {
|
||||||
|
score += matchFollowingSeparatorBonus
|
||||||
|
}
|
||||||
|
if len(match.MatchedIndexes) > 0 {
|
||||||
|
lastMatch := match.MatchedIndexes[len(match.MatchedIndexes)-1]
|
||||||
|
bonus := adjacentCharBonus(lastIndex, lastMatch, currAdjacentMatchBonus)
|
||||||
|
score += bonus
|
||||||
|
// adjacent matches are incremental and keep increasing based on previous adjacent matches
|
||||||
|
// thus we need to maintain the current match bonus
|
||||||
|
currAdjacentMatchBonus += bonus
|
||||||
|
}
|
||||||
|
if score > bestScore {
|
||||||
|
bestScore = score
|
||||||
|
matchedIndex = j
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var nextp rune
|
||||||
|
if patternIndex < len(runes)-1 {
|
||||||
|
nextp = runes[patternIndex+1]
|
||||||
|
}
|
||||||
|
if j+candidateSize < len(data.String(i)) {
|
||||||
|
if data.String(i)[j+candidateSize] < utf8.RuneSelf { // Fast path for ASCII
|
||||||
|
nextc, nextSize = rune(data.String(i)[j+candidateSize]), 1
|
||||||
|
} else {
|
||||||
|
nextc, nextSize = utf8.DecodeRuneInString(data.String(i)[j+candidateSize:])
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
nextc, nextSize = 0, 0
|
||||||
|
}
|
||||||
|
// We apply the best score when we have the next match coming up or when the search string has ended.
|
||||||
|
// Tracking when the next match is coming up allows us to exhaustively find the best match and not necessarily
|
||||||
|
// the first match.
|
||||||
|
// For example given the pattern "tk" and search string "The Black Knight", exhaustively matching allows us
|
||||||
|
// to match the second k thus giving this string a higher score.
|
||||||
|
if equalFold(nextp, nextc) || nextc == 0 {
|
||||||
|
if matchedIndex > -1 {
|
||||||
|
if len(match.MatchedIndexes) == 0 {
|
||||||
|
penalty := matchedIndex * unmatchedLeadingCharPenalty
|
||||||
|
bestScore += max(penalty, maxUnmatchedLeadingCharPenalty)
|
||||||
|
}
|
||||||
|
match.Score += bestScore
|
||||||
|
match.MatchedIndexes = append(match.MatchedIndexes, matchedIndex)
|
||||||
|
score = 0
|
||||||
|
bestScore = -1
|
||||||
|
patternIndex++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lastIndex = j
|
||||||
|
last = candidate
|
||||||
|
}
|
||||||
|
// apply penalty for each unmatched character
|
||||||
|
penalty := len(match.MatchedIndexes) - len(data.String(i))
|
||||||
|
match.Score += penalty
|
||||||
|
if len(match.MatchedIndexes) == len(runes) {
|
||||||
|
matches = append(matches, match)
|
||||||
|
matchedIndexes = nil
|
||||||
|
} else {
|
||||||
|
matchedIndexes = match.MatchedIndexes[:0] // Recycle match index slice
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return matches
|
||||||
|
}
|
||||||
|
|
||||||
|
// Taken from strings.EqualFold
|
||||||
|
func equalFold(tr, sr rune) bool {
|
||||||
|
if tr == sr {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if tr < sr {
|
||||||
|
tr, sr = sr, tr
|
||||||
|
}
|
||||||
|
// Fast check for ASCII.
|
||||||
|
if tr < utf8.RuneSelf {
|
||||||
|
// ASCII, and sr is upper case. tr must be lower case.
|
||||||
|
if 'A' <= sr && sr <= 'Z' && tr == sr+'a'-'A' {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// General case. SimpleFold(x) returns the next equivalent rune > x
|
||||||
|
// or wraps around to smaller values.
|
||||||
|
r := unicode.SimpleFold(sr)
|
||||||
|
for r != sr && r < tr {
|
||||||
|
r = unicode.SimpleFold(r)
|
||||||
|
}
|
||||||
|
return r == tr
|
||||||
|
}
|
||||||
|
|
||||||
|
func adjacentCharBonus(i int, lastMatch int, currentBonus int) int {
|
||||||
|
if lastMatch == i {
|
||||||
|
return currentBonus*2 + adjacentMatchBonus
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func isSeparator(s rune) bool {
|
||||||
|
for _, sep := range separators {
|
||||||
|
if s == sep {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func max(x int, y int) int {
|
||||||
|
if x > y {
|
||||||
|
return x
|
||||||
|
}
|
||||||
|
return y
|
||||||
|
}
|
||||||
14
vendor/modules.txt
vendored
14
vendor/modules.txt
vendored
@ -71,7 +71,10 @@ github.com/cespare/xxhash/v2
|
|||||||
# github.com/charmbracelet/bubbles v0.21.0
|
# github.com/charmbracelet/bubbles v0.21.0
|
||||||
## explicit; go 1.23.0
|
## explicit; go 1.23.0
|
||||||
github.com/charmbracelet/bubbles/cursor
|
github.com/charmbracelet/bubbles/cursor
|
||||||
|
github.com/charmbracelet/bubbles/help
|
||||||
github.com/charmbracelet/bubbles/key
|
github.com/charmbracelet/bubbles/key
|
||||||
|
github.com/charmbracelet/bubbles/list
|
||||||
|
github.com/charmbracelet/bubbles/paginator
|
||||||
github.com/charmbracelet/bubbles/runeutil
|
github.com/charmbracelet/bubbles/runeutil
|
||||||
github.com/charmbracelet/bubbles/spinner
|
github.com/charmbracelet/bubbles/spinner
|
||||||
github.com/charmbracelet/bubbles/textinput
|
github.com/charmbracelet/bubbles/textinput
|
||||||
@ -283,9 +286,6 @@ github.com/emirpasic/gods/utils
|
|||||||
# github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f
|
# github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f
|
||||||
## explicit; go 1.16
|
## explicit; go 1.16
|
||||||
github.com/erikgeiser/coninput
|
github.com/erikgeiser/coninput
|
||||||
# github.com/evertras/bubble-table v0.19.2
|
|
||||||
## explicit; go 1.18
|
|
||||||
github.com/evertras/bubble-table/table
|
|
||||||
# github.com/felixge/httpsnoop v1.0.4
|
# github.com/felixge/httpsnoop v1.0.4
|
||||||
## explicit; go 1.13
|
## explicit; go 1.13
|
||||||
github.com/felixge/httpsnoop
|
github.com/felixge/httpsnoop
|
||||||
@ -493,11 +493,6 @@ github.com/muesli/ansi/compressor
|
|||||||
# github.com/muesli/cancelreader v0.2.2
|
# github.com/muesli/cancelreader v0.2.2
|
||||||
## explicit; go 1.17
|
## explicit; go 1.17
|
||||||
github.com/muesli/cancelreader
|
github.com/muesli/cancelreader
|
||||||
# github.com/muesli/reflow v0.3.0
|
|
||||||
## explicit; go 1.13
|
|
||||||
github.com/muesli/reflow/ansi
|
|
||||||
github.com/muesli/reflow/truncate
|
|
||||||
github.com/muesli/reflow/wordwrap
|
|
||||||
# github.com/muesli/termenv v0.16.0
|
# github.com/muesli/termenv v0.16.0
|
||||||
## explicit; go 1.17
|
## explicit; go 1.17
|
||||||
github.com/muesli/termenv
|
github.com/muesli/termenv
|
||||||
@ -552,6 +547,9 @@ github.com/rivo/uniseg
|
|||||||
# github.com/russross/blackfriday/v2 v2.1.0
|
# github.com/russross/blackfriday/v2 v2.1.0
|
||||||
## explicit
|
## explicit
|
||||||
github.com/russross/blackfriday/v2
|
github.com/russross/blackfriday/v2
|
||||||
|
# github.com/sahilm/fuzzy v0.1.1
|
||||||
|
## explicit
|
||||||
|
github.com/sahilm/fuzzy
|
||||||
# github.com/schollz/progressbar/v3 v3.18.0
|
# github.com/schollz/progressbar/v3 v3.18.0
|
||||||
## explicit; go 1.22
|
## explicit; go 1.22
|
||||||
github.com/schollz/progressbar/v3
|
github.com/schollz/progressbar/v3
|
||||||
|
|||||||
Reference in New Issue
Block a user