Compare commits
1 Commits
Author | SHA1 | Date | |
---|---|---|---|
7c5a83bce2
|
430
cli/app/list.go
430
cli/app/list.go
@ -3,22 +3,22 @@ package app
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"coopcloud.tech/abra/cli/internal"
|
||||
appPkg "coopcloud.tech/abra/pkg/app"
|
||||
"coopcloud.tech/abra/pkg/autocomplete"
|
||||
"coopcloud.tech/abra/pkg/config"
|
||||
"coopcloud.tech/abra/pkg/i18n"
|
||||
"coopcloud.tech/abra/pkg/log"
|
||||
"coopcloud.tech/abra/pkg/recipe"
|
||||
"coopcloud.tech/tagcmp"
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
"github.com/charmbracelet/bubbles/spinner"
|
||||
"github.com/charmbracelet/bubbles/list"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/evertras/bubble-table/table"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@ -56,15 +56,15 @@ var AppListCommand = &cobra.Command{
|
||||
Short: i18n.G("List 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
|
||||
abra app ls
|
||||
|
||||
# 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
|
||||
abra app ls -r gitea`),
|
||||
abra app ls r gitea`),
|
||||
Args: cobra.NoArgs,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
appFiles, err := appPkg.LoadAppFiles(listAppServer)
|
||||
@ -221,7 +221,7 @@ Use "--status/-S" flag to query all servers for the live deployment status.`),
|
||||
return
|
||||
}
|
||||
|
||||
if err := runTable(apps); err != nil {
|
||||
if err := runTui(apps); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
},
|
||||
@ -262,7 +262,7 @@ func init() {
|
||||
i18n.G("machine"),
|
||||
i18n.G("m"),
|
||||
false,
|
||||
i18n.G("print machine-readable output"),
|
||||
i18n.G("print machinereadable output"),
|
||||
)
|
||||
|
||||
AppListCommand.Flags().StringVarP(
|
||||
@ -281,45 +281,127 @@ func init() {
|
||||
)
|
||||
}
|
||||
|
||||
func getNumServersAndRecipes(apps []appPkg.App) (int, int) {
|
||||
var (
|
||||
servers []string
|
||||
recipes []string
|
||||
)
|
||||
var listRenderStyle = lipgloss.NewStyle().Margin(1, 2)
|
||||
|
||||
for _, app := range apps {
|
||||
if !slices.Contains(servers, app.Server) {
|
||||
servers = append(servers, app.Server)
|
||||
}
|
||||
if !slices.Contains(recipes, app.Recipe.Name) {
|
||||
recipes = append(recipes, app.Recipe.Name)
|
||||
}
|
||||
}
|
||||
|
||||
return len(servers), len(recipes)
|
||||
type appMeta struct {
|
||||
status string
|
||||
version string
|
||||
updates string
|
||||
chaos string
|
||||
}
|
||||
|
||||
type errorMsg struct{ err error }
|
||||
type listItem struct {
|
||||
title string
|
||||
app appPkg.App
|
||||
appMetadata appMeta
|
||||
}
|
||||
|
||||
func (e errorMsg) Error() string { return e.err.Error() }
|
||||
func (li listItem) Title() string { return li.title }
|
||||
func (li listItem) Description() string {
|
||||
return fmt.Sprintf(
|
||||
"server: %s • recipe: %s • status: %s • version: %s • updates: %s • chaos: %s",
|
||||
li.app.Server,
|
||||
li.app.Recipe.Name,
|
||||
li.appMetadata.status,
|
||||
li.appMetadata.version,
|
||||
li.appMetadata.updates,
|
||||
li.appMetadata.chaos,
|
||||
)
|
||||
}
|
||||
func (li listItem) FilterValue() string {
|
||||
return fmt.Sprintf(
|
||||
"%s %s %s %s %s %s %s",
|
||||
li.app.Domain,
|
||||
li.app.Server,
|
||||
li.app.Recipe.Name,
|
||||
li.appMetadata.status,
|
||||
li.appMetadata.version,
|
||||
li.appMetadata.updates,
|
||||
li.appMetadata.chaos,
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
func getAppsDeployStatus(m model) tea.Msg {
|
||||
var apps []appPkg.App
|
||||
|
||||
for _, row := range m.table.GetVisibleRows() {
|
||||
apps = append(apps, row.Data["app"].(appPkg.App))
|
||||
for _, li := range m.list.VisibleItems() {
|
||||
i := li.(listItem) // NOTE(d1): convert back to our custom item
|
||||
apps = append(apps, i.app)
|
||||
}
|
||||
|
||||
statuses, err := appPkg.GetAppStatuses(apps, true)
|
||||
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)
|
||||
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 {
|
||||
@ -328,7 +410,8 @@ func getAppsDeployStatus(m model) tea.Msg {
|
||||
if version, ok := status["version"]; ok {
|
||||
updates, err := recipe.GetRecipeCatalogueVersions(app.Recipe.Name, catl)
|
||||
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)
|
||||
@ -357,238 +440,109 @@ func getAppsDeployStatus(m model) tea.Msg {
|
||||
return appsDeployStatusMsg(statuses)
|
||||
}
|
||||
|
||||
func renderAppsDeployStatus(m *model, appStatuses appsDeployStatusMsg) table.Model {
|
||||
for _, row := range m.table.GetVisibleRows() {
|
||||
app := row.Data["app"].(appPkg.App)
|
||||
appStatus := appStatuses[app.StackName()]
|
||||
func renderAppsDeployStatus(m model, appStatuses appsDeployStatusMsg) []list.Item {
|
||||
itemsWithStatus := make(map[string]list.Item)
|
||||
|
||||
for _, li := range m.list.VisibleItems() {
|
||||
i := li.(listItem) // NOTE(d1): convert back to our custom item
|
||||
|
||||
appStatus := appStatuses[i.app.StackName()]
|
||||
|
||||
var (
|
||||
version = appStatus["version"]
|
||||
updates = appStatus["updates"]
|
||||
status = appStatus["status"]
|
||||
chaos = appStatus["chaos"]
|
||||
chaosVersion = appStatus["chaosVersion"]
|
||||
autoUpdate = appStatus["autoUpdate"]
|
||||
appVersion = appStatus["version"]
|
||||
appUpdates = appStatus["updates"]
|
||||
appDeploymentStatus = appStatus["status"]
|
||||
appChaosVersion = appStatus["chaosVersion"]
|
||||
)
|
||||
|
||||
if status != "" {
|
||||
row.Data["status"] = status
|
||||
if appVersion == "" {
|
||||
appVersion = "-"
|
||||
}
|
||||
|
||||
if version != "" {
|
||||
row.Data["version"] = version
|
||||
row.Data["updates"] = updates
|
||||
if appDeploymentStatus == "" || appDeploymentStatus == config.UNKNOWN_DEFAULT {
|
||||
appDeploymentStatus = "-"
|
||||
}
|
||||
|
||||
if chaos != "" {
|
||||
if chaosVersion != "" {
|
||||
row.Data["chaos-version"] = chaosVersion
|
||||
}
|
||||
if appChaosVersion == "" {
|
||||
appChaosVersion = "-"
|
||||
}
|
||||
|
||||
if autoUpdate != "" {
|
||||
row.Data["autoUpdate"] = autoUpdate
|
||||
if len(appUpdates) == 0 {
|
||||
appUpdates = "-"
|
||||
} else {
|
||||
appUpdates = fmt.Sprintf("%d", len(appUpdates))
|
||||
}
|
||||
|
||||
newMetadata := appMeta{
|
||||
status: appDeploymentStatus,
|
||||
version: appVersion,
|
||||
updates: appUpdates,
|
||||
chaos: appChaosVersion,
|
||||
}
|
||||
|
||||
itemsWithStatus[i.title] = listItem{
|
||||
title: i.title,
|
||||
app: i.app,
|
||||
appMetadata: newMetadata,
|
||||
}
|
||||
}
|
||||
return m.table
|
||||
|
||||
var items []list.Item
|
||||
for _, li := range m.list.Items() {
|
||||
i := li.(listItem) // NOTE(d1): convert back to our custom item
|
||||
|
||||
if is, ok := itemsWithStatus[i.title]; ok {
|
||||
items = append(items, is)
|
||||
continue
|
||||
}
|
||||
|
||||
items = append(items, i)
|
||||
}
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
type initTableMsg struct{ table table.Model }
|
||||
func runTui(apps []appPkg.App) error {
|
||||
var items []list.Item
|
||||
|
||||
func initTable(m model) tea.Msg {
|
||||
var rows []table.Row
|
||||
sort.Sort(appPkg.ByServerAndDomain(apps))
|
||||
|
||||
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: "-",
|
||||
for _, app := range apps {
|
||||
items = append(items, listItem{
|
||||
title: app.Domain,
|
||||
app: app,
|
||||
appMetadata: appMeta{
|
||||
status: "-",
|
||||
version: "-",
|
||||
updates: "-",
|
||||
chaos: "-",
|
||||
},
|
||||
})
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
// NOTE(d1): disable standard styles on filter match since we
|
||||
// hack the description to also become a target
|
||||
delegate := list.NewDefaultDelegate()
|
||||
delegateStyles := list.NewDefaultItemStyles()
|
||||
delegateStyles.FilterMatch = delegateStyles.NormalTitle
|
||||
delegate.Styles = delegateStyles
|
||||
|
||||
func (m model) Init() tea.Cmd {
|
||||
return tea.Batch(
|
||||
func() tea.Msg { return initTable(m) },
|
||||
m.spinner.Tick,
|
||||
)
|
||||
}
|
||||
l := list.New(items, delegate, 0, 0)
|
||||
l.Title = config.ABRA_DIR
|
||||
l.StatusMessageLifetime = time.Second * 3
|
||||
l.SetStatusBarItemName("app", "apps")
|
||||
|
||||
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) }
|
||||
}
|
||||
listKeys := newListKeyMap()
|
||||
l.AdditionalShortHelpKeys = func() []key.Binding {
|
||||
return []key.Binding{
|
||||
listKeys.status,
|
||||
}
|
||||
case initTableMsg:
|
||||
m.table = msg.table
|
||||
|
||||
m.table = m.table.WithTargetWidth(m.width)
|
||||
m.table = m.table.WithPageSize(calculateHeight(m))
|
||||
|
||||
if m.initStatusGather {
|
||||
m.pollingStatus = true
|
||||
return m, func() tea.Msg { return getAppsDeployStatus(m) }
|
||||
}
|
||||
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{
|
||||
apps: apps,
|
||||
numApps: numApps,
|
||||
numServers: numServers,
|
||||
numRecipes: numRecipes,
|
||||
numFilteredApps: numApps,
|
||||
numFilteredServers: numServers,
|
||||
numFilteredRecipes: numRecipes,
|
||||
spinner: s,
|
||||
initStatusGather: status,
|
||||
apps: apps,
|
||||
list: l,
|
||||
initStatusGather: status,
|
||||
}
|
||||
|
||||
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/docker v28.4.0+incompatible
|
||||
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/google/go-cmp v0.7.0
|
||||
github.com/leonelquinteros/gotext v1.7.2
|
||||
@ -94,7 +93,6 @@ require (
|
||||
github.com/morikuni/aec v1.0.0 // indirect
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // 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/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // 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/rivo/uniseg v0.4.7 // 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/skeema/knownhosts v1.3.1 // 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/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/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.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
|
||||
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/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.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||
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-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/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||
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/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
||||
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/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=
|
||||
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/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
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/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
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/schollz/progressbar/v3 v3.18.0 h1:uXdoHABRFmNIjUfte/Ex7WtuyVslrw2wVPQmCN62HpA=
|
||||
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)
|
||||
}
|
||||
|
||||
// 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
|
||||
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
|
||||
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
|
||||
## explicit; go 1.23.0
|
||||
github.com/charmbracelet/bubbles/cursor
|
||||
github.com/charmbracelet/bubbles/help
|
||||
github.com/charmbracelet/bubbles/key
|
||||
github.com/charmbracelet/bubbles/list
|
||||
github.com/charmbracelet/bubbles/paginator
|
||||
github.com/charmbracelet/bubbles/runeutil
|
||||
github.com/charmbracelet/bubbles/spinner
|
||||
github.com/charmbracelet/bubbles/textinput
|
||||
@ -283,9 +286,6 @@ github.com/emirpasic/gods/utils
|
||||
# github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f
|
||||
## explicit; go 1.16
|
||||
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
|
||||
## explicit; go 1.13
|
||||
github.com/felixge/httpsnoop
|
||||
@ -493,11 +493,6 @@ github.com/muesli/ansi/compressor
|
||||
# github.com/muesli/cancelreader v0.2.2
|
||||
## explicit; go 1.17
|
||||
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
|
||||
## explicit; go 1.17
|
||||
github.com/muesli/termenv
|
||||
@ -552,6 +547,9 @@ github.com/rivo/uniseg
|
||||
# github.com/russross/blackfriday/v2 v2.1.0
|
||||
## explicit
|
||||
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
|
||||
## explicit; go 1.22
|
||||
github.com/schollz/progressbar/v3
|
||||
|
Reference in New Issue
Block a user