From e68fc20e7207dc9fe70ba97a007b6c31d413942b Mon Sep 17 00:00:00 2001 From: decentral1se Date: Wed, 15 Oct 2025 13:57:48 +0200 Subject: [PATCH] fix: ust list from bubbles See https://git.coopcloud.tech/toolshed/abra/issues/691 --- cli/app/list.go | 416 +++--- go.mod | 3 +- go.sum | 9 +- pkg/app/app.go | 12 + .../charmbracelet/bubbles/help/help.go | 240 +++ .../charmbracelet/bubbles/list/README.md | 71 + .../charmbracelet/bubbles/list/defaultitem.go | 229 +++ .../charmbracelet/bubbles/list/keys.go | 97 ++ .../charmbracelet/bubbles/list/list.go | 1328 +++++++++++++++++ .../charmbracelet/bubbles/list/style.go | 99 ++ .../bubbles/paginator/paginator.go | 214 +++ .../evertras/bubble-table/table/border.go | 439 ------ .../evertras/bubble-table/table/calc.go | 36 - .../evertras/bubble-table/table/cell.go | 60 - .../evertras/bubble-table/table/column.go | 118 -- .../evertras/bubble-table/table/data.go | 67 - .../evertras/bubble-table/table/dimensions.go | 116 -- .../evertras/bubble-table/table/doc.go | 39 - .../evertras/bubble-table/table/events.go | 60 - .../evertras/bubble-table/table/filter.go | 164 -- .../evertras/bubble-table/table/footer.go | 51 - .../evertras/bubble-table/table/header.go | 93 -- .../evertras/bubble-table/table/keys.go | 120 -- .../evertras/bubble-table/table/model.go | 148 -- .../evertras/bubble-table/table/options.go | 510 ------- .../evertras/bubble-table/table/overflow.go | 18 - .../evertras/bubble-table/table/pagination.go | 112 -- .../evertras/bubble-table/table/query.go | 96 -- .../evertras/bubble-table/table/row.go | 252 ---- .../evertras/bubble-table/table/scrolling.go | 50 - .../evertras/bubble-table/table/sort.go | 178 --- .../evertras/bubble-table/table/strlimit.go | 26 - .../evertras/bubble-table/table/update.go | 154 -- .../evertras/bubble-table/table/view.go | 65 - vendor/github.com/muesli/reflow/LICENSE | 21 - vendor/github.com/muesli/reflow/ansi/ansi.go | 7 - .../github.com/muesli/reflow/ansi/buffer.go | 40 - .../github.com/muesli/reflow/ansi/writer.go | 76 - .../muesli/reflow/truncate/truncate.go | 120 -- .../muesli/reflow/wordwrap/wordwrap.go | 167 --- vendor/github.com/sahilm/fuzzy/.editorconfig | 18 + vendor/github.com/sahilm/fuzzy/.gitignore | 2 + vendor/github.com/sahilm/fuzzy/.travis.yml | 8 + .../github.com/sahilm/fuzzy/CONTRIBUTING.md | 1 + vendor/github.com/sahilm/fuzzy/Gopkg.lock | 20 + vendor/github.com/sahilm/fuzzy/Gopkg.toml | 4 + .../bubble-table => sahilm/fuzzy}/LICENSE | 4 +- vendor/github.com/sahilm/fuzzy/Makefile | 57 + vendor/github.com/sahilm/fuzzy/README.md | 186 +++ vendor/github.com/sahilm/fuzzy/fuzzy.go | 254 ++++ vendor/modules.txt | 14 +- 51 files changed, 3025 insertions(+), 3664 deletions(-) create mode 100644 vendor/github.com/charmbracelet/bubbles/help/help.go create mode 100644 vendor/github.com/charmbracelet/bubbles/list/README.md create mode 100644 vendor/github.com/charmbracelet/bubbles/list/defaultitem.go create mode 100644 vendor/github.com/charmbracelet/bubbles/list/keys.go create mode 100644 vendor/github.com/charmbracelet/bubbles/list/list.go create mode 100644 vendor/github.com/charmbracelet/bubbles/list/style.go create mode 100644 vendor/github.com/charmbracelet/bubbles/paginator/paginator.go delete mode 100644 vendor/github.com/evertras/bubble-table/table/border.go delete mode 100644 vendor/github.com/evertras/bubble-table/table/calc.go delete mode 100644 vendor/github.com/evertras/bubble-table/table/cell.go delete mode 100644 vendor/github.com/evertras/bubble-table/table/column.go delete mode 100644 vendor/github.com/evertras/bubble-table/table/data.go delete mode 100644 vendor/github.com/evertras/bubble-table/table/dimensions.go delete mode 100644 vendor/github.com/evertras/bubble-table/table/doc.go delete mode 100644 vendor/github.com/evertras/bubble-table/table/events.go delete mode 100644 vendor/github.com/evertras/bubble-table/table/filter.go delete mode 100644 vendor/github.com/evertras/bubble-table/table/footer.go delete mode 100644 vendor/github.com/evertras/bubble-table/table/header.go delete mode 100644 vendor/github.com/evertras/bubble-table/table/keys.go delete mode 100644 vendor/github.com/evertras/bubble-table/table/model.go delete mode 100644 vendor/github.com/evertras/bubble-table/table/options.go delete mode 100644 vendor/github.com/evertras/bubble-table/table/overflow.go delete mode 100644 vendor/github.com/evertras/bubble-table/table/pagination.go delete mode 100644 vendor/github.com/evertras/bubble-table/table/query.go delete mode 100644 vendor/github.com/evertras/bubble-table/table/row.go delete mode 100644 vendor/github.com/evertras/bubble-table/table/scrolling.go delete mode 100644 vendor/github.com/evertras/bubble-table/table/sort.go delete mode 100644 vendor/github.com/evertras/bubble-table/table/strlimit.go delete mode 100644 vendor/github.com/evertras/bubble-table/table/update.go delete mode 100644 vendor/github.com/evertras/bubble-table/table/view.go delete mode 100644 vendor/github.com/muesli/reflow/LICENSE delete mode 100644 vendor/github.com/muesli/reflow/ansi/ansi.go delete mode 100644 vendor/github.com/muesli/reflow/ansi/buffer.go delete mode 100644 vendor/github.com/muesli/reflow/ansi/writer.go delete mode 100644 vendor/github.com/muesli/reflow/truncate/truncate.go delete mode 100644 vendor/github.com/muesli/reflow/wordwrap/wordwrap.go create mode 100644 vendor/github.com/sahilm/fuzzy/.editorconfig create mode 100644 vendor/github.com/sahilm/fuzzy/.gitignore create mode 100644 vendor/github.com/sahilm/fuzzy/.travis.yml create mode 100644 vendor/github.com/sahilm/fuzzy/CONTRIBUTING.md create mode 100644 vendor/github.com/sahilm/fuzzy/Gopkg.lock create mode 100644 vendor/github.com/sahilm/fuzzy/Gopkg.toml rename vendor/github.com/{evertras/bubble-table => sahilm/fuzzy}/LICENSE (94%) create mode 100644 vendor/github.com/sahilm/fuzzy/Makefile create mode 100644 vendor/github.com/sahilm/fuzzy/README.md create mode 100644 vendor/github.com/sahilm/fuzzy/fuzzy.go diff --git a/cli/app/list.go b/cli/app/list.go index aeba022d..a12ea944 100644 --- a/cli/app/list.go +++ b/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,99 @@ 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 listItem struct { + title string + app appPkg.App + appMetadata string } -type errorMsg struct{ err error } +func (li listItem) Title() string { return li.title } +func (li listItem) Description() string { return li.appMetadata } +func (li listItem) FilterValue() string { return li.title + " " + li.appMetadata } -func (e errorMsg) Error() string { return e.err.Error() } +type model struct { + apps []appPkg.App + list list.Model + initStatusGather bool + pollingStatus bool + err error +} + +func (m model) Init() tea.Cmd { + return nil +} + +func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmd tea.Cmd + var cmds []tea.Cmd + + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.String() { + case "s": + if !m.pollingStatus { + m.pollingStatus = true + cmds = append(cmds, m.list.NewStatusMessage("Looking up app statuses 🧐")) + cmds = append(cmds, func() tea.Msg { return getAppsDeployStatus(m) }) + } else { + cmds = append(cmds, m.list.NewStatusMessage("In progress, please hold ✋")) + } + case "q": + return m, tea.Quit + } + case appsDeployStatusMsg: + m.pollingStatus = false + cmds = append(cmds, m.list.SetItems(renderAppsDeployStatus(m, msg))) + case tea.WindowSizeMsg: + h, v := listRenderStyle.GetFrameSize() + m.list.SetSize(msg.Width-h, msg.Height-v) + } + + m.list, cmd = m.list.Update(msg) + cmds = append(cmds, cmd) + + return m, tea.Batch(cmds...) +} + +func (m model) View() string { + return listRenderStyle.Render(m.list.View()) +} + +type listKeyMap struct { + status key.Binding +} + +func newListKeyMap() *listKeyMap { + return &listKeyMap{ + status: key.NewBinding( + key.WithKeys("s"), + key.WithHelp("s", "status"), + ), + } +} type appsDeployStatusMsg map[string]map[string]string 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 +382,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 +412,115 @@ 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 := fmt.Sprintf( + "server: %s • recipe: %s • status: %s • version: %s • updates: %s • chaos: %s", + i.app.Server, + i.app.Recipe.Name, + appDeploymentStatus, + appVersion, + appUpdates, + appChaosVersion, + ) + + itemsWithStatus[i.title] = listItem{ + title: i.title, + app: i.app, + appMetadata: newMetadata, } } - return m.table -} -type initTableMsg struct{ table table.Model } + var items []list.Item + for _, li := range m.list.Items() { + i := li.(listItem) // NOTE(d1): convert back to our custom item -func initTable(m model) tea.Msg { - var rows []table.Row - - for _, app := range m.apps { - rows = append(rows, table.NewRow(table.RowData{ - "domain": app.Domain, - "server": app.Server, - "recipe": app.Recipe.Name, - "app": app, - })) - } - - colStyle := lipgloss.NewStyle().Align(lipgloss.Left) - columns := []table.Column{ - table.NewFlexColumn("domain", "DOMAIN", 2).WithFiltered(true).WithStyle(colStyle), - table.NewFlexColumn("server", "SERVER", 1).WithFiltered(true).WithStyle(colStyle), - table.NewFlexColumn("recipe", "RECIPE", 1).WithFiltered(true).WithStyle(colStyle), - table.NewFlexColumn("status", "STATUS", 1).WithFiltered(true).WithStyle(colStyle), - table.NewFlexColumn("version", "VERSION", 1).WithFiltered(true).WithStyle(colStyle), - table.NewFlexColumn("updates", "UPDATES", 1).WithFiltered(true).WithStyle(colStyle), - table.NewFlexColumn("chaos-version", "CHAOS", 1).WithFiltered(true).WithStyle(colStyle), - table.NewFlexColumn("auto-update", "AUTO-UPDATE", 1).WithFiltered(true).WithStyle(colStyle), - } - - keymap := table.DefaultKeyMap() - keymap.Filter = key.NewBinding(key.WithKeys("/", "f")) - keymap.PageDown = key.NewBinding(key.WithKeys("right", "l", "pgdown", "ctrl+d")) - keymap.PageUp = key.NewBinding(key.WithKeys("left", "h", "pgup", "ctrl+u")) - - t := table. - New(columns). - Filtered(true). - Focused(true). - WithRows([]table.Row(rows)). - WithKeyMap(keymap). - WithMultiline(true). - WithFuzzyFilter(). - SortByAsc("domain"). - WithNoPagination(). - WithMissingDataIndicatorStyled(table.StyledCell{ - Style: lipgloss.NewStyle().Foreground(lipgloss.Color("#faa")), - Data: "-", - }) - - return initTableMsg{table: t} -} - -type model struct { - apps []appPkg.App - - numApps int - numServers int - numRecipes int - - numFilteredApps int - numFilteredServers int - numFilteredRecipes int - - initStatusGather bool - - table table.Model - spinner spinner.Model - pollingStatus bool - - width int - height int - - err error -} - -func (m model) getFilteredApps() []appPkg.App { - var servers []appPkg.App - for _, row := range m.table.GetVisibleRows() { - servers = append(servers, row.Data["app"].(appPkg.App)) - } - return servers -} - -func (m *model) updateCount() { - if m.table.GetIsFilterActive() { - apps := m.getFilteredApps() - m.numFilteredApps = len(apps) - m.numFilteredServers, m.numFilteredRecipes = getNumServersAndRecipes(apps) - } else { - m.numFilteredApps = m.numApps - m.numFilteredServers = m.numServers - m.numFilteredRecipes = m.numRecipes - } -} - -func (m model) Init() tea.Cmd { - return tea.Batch( - func() tea.Msg { return initTable(m) }, - m.spinner.Tick, - ) -} - -func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - var cmd tea.Cmd - var cmds []tea.Cmd - - switch msg := msg.(type) { - case tea.KeyMsg: - m.updateCount() - - switch msg.String() { - case "q": - return m, tea.Quit - case "s": - if !m.table.GetIsFilterInputFocused() { - m.pollingStatus = true - return m, func() tea.Msg { return getAppsDeployStatus(m) } - } + if is, ok := itemsWithStatus[i.title]; ok { + items = append(items, is) + continue } - case initTableMsg: - m.table = msg.table - m.table = m.table.WithTargetWidth(m.width) - m.table = m.table.WithPageSize(calculateHeight(m)) + items = append(items, i) + } - if m.initStatusGather { - m.pollingStatus = true - return m, func() tea.Msg { return getAppsDeployStatus(m) } + return items +} + +func runTui(apps []appPkg.App) error { + var items []list.Item + + sort.Sort(appPkg.ByServerAndDomain(apps)) + + for _, app := range apps { + items = append(items, listItem{ + title: app.Domain, + app: app, + appMetadata: fmt.Sprintf( + "server: %s • recipe: %s • status: %s • version: %s • updates: %s • chaos: %s", + app.Server, + app.Recipe.Name, + "-", + "-", + "-", + "-", + ), + }, + ) + } + + delegate := list.NewDefaultDelegate() + delegateStyles := list.NewDefaultItemStyles() + delegateStyles.FilterMatch = delegateStyles.NormalTitle + delegate.Styles = delegateStyles + + l := list.New(items, delegate, 0, 0) + l.Title = config.ABRA_DIR + l.StatusMessageLifetime = time.Second * 3 + l.SetStatusBarItemName("app", "apps") + l.SetShowFilter(false) + + listKeys := newListKeyMap() + l.AdditionalShortHelpKeys = func() []key.Binding { + return []key.Binding{ + listKeys.status, } - case appsDeployStatusMsg: - m.pollingStatus = false - m.table = renderAppsDeployStatus(&m, msg) - case tea.WindowSizeMsg: - m.width = msg.Width - m.height = msg.Height - - m.table = m.table.WithTargetWidth(m.width) - m.table = m.table.WithPageSize(calculateHeight(m)) - case errorMsg: - m.err = msg } - m.table, cmd = m.table.Update(msg) - cmds = append(cmds, cmd) - - m.spinner, cmd = m.spinner.Update(msg) - cmds = append(cmds, cmd) - - return m, tea.Batch(cmds...) -} - -func calculateHeight(m model) int { - return m.height/2 - 5 -} - -func (m model) View() string { - if m.err != nil { - return fmt.Sprintf("FATA: %v", m.err) - } - - body := strings.Builder{} - - body.WriteString(m.table.View() + "\n") - - stats := fmt.Sprintf( - "[servers] %v • [apps] %v • [recipes] %v", - m.numFilteredServers, m.numFilteredApps, m.numFilteredRecipes, - ) - - help := "[q] quit • [/] filter • [s] status • [ctrl+u/d] page up/down" - - body.WriteString(lipgloss.JoinHorizontal(lipgloss.Center, stats, " | ", help)) - - if m.pollingStatus { - body.WriteString(fmt.Sprintf(" | %s querying app status", m.spinner.View())) - } else { - body.WriteString(" | -") - } - - return body.String() -} - -func runTable(apps []appPkg.App) error { - s := spinner.New() - s.Spinner = spinner.Dot - s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("205")) - - numServers, numRecipes := getNumServersAndRecipes(apps) - numApps := len(apps) - m := model{ - 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()) diff --git a/go.mod b/go.mod index ee81b55b..0ef8892e 100644 --- a/go.mod +++ b/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 diff --git a/go.sum b/go.sum index 6e15adf5..b11afb86 100644 --- a/go.sum +++ b/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= diff --git a/pkg/app/app.go b/pkg/app/app.go index a73a853d..39c434c3 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -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 diff --git a/vendor/github.com/charmbracelet/bubbles/help/help.go b/vendor/github.com/charmbracelet/bubbles/help/help.go new file mode 100644 index 00000000..bb402a6c --- /dev/null +++ b/vendor/github.com/charmbracelet/bubbles/help/help.go @@ -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 +} diff --git a/vendor/github.com/charmbracelet/bubbles/list/README.md b/vendor/github.com/charmbracelet/bubbles/list/README.md new file mode 100644 index 00000000..7dac8e7b --- /dev/null +++ b/vendor/github.com/charmbracelet/bubbles/list/README.md @@ -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 diff --git a/vendor/github.com/charmbracelet/bubbles/list/defaultitem.go b/vendor/github.com/charmbracelet/bubbles/list/defaultitem.go new file mode 100644 index 00000000..4affe342 --- /dev/null +++ b/vendor/github.com/charmbracelet/bubbles/list/defaultitem.go @@ -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 +} diff --git a/vendor/github.com/charmbracelet/bubbles/list/keys.go b/vendor/github.com/charmbracelet/bubbles/list/keys.go new file mode 100644 index 00000000..33220313 --- /dev/null +++ b/vendor/github.com/charmbracelet/bubbles/list/keys.go @@ -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")), + } +} diff --git a/vendor/github.com/charmbracelet/bubbles/list/list.go b/vendor/github.com/charmbracelet/bubbles/list/list.go new file mode 100644 index 00000000..a96dc77f --- /dev/null +++ b/vendor/github.com/charmbracelet/bubbles/list/list.go @@ -0,0 +1,1328 @@ +// Package list provides a feature-rich Bubble Tea component for browsing +// a general purpose list of items. It features optional filtering, pagination, +// help, status messages, and a spinner to indicate activity. +package list + +import ( + "fmt" + "io" + "sort" + "strings" + "time" + + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/charmbracelet/x/ansi" + "github.com/sahilm/fuzzy" + + "github.com/charmbracelet/bubbles/help" + "github.com/charmbracelet/bubbles/key" + "github.com/charmbracelet/bubbles/paginator" + "github.com/charmbracelet/bubbles/spinner" + "github.com/charmbracelet/bubbles/textinput" +) + +// 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 +} + +// ItemDelegate encapsulates the general functionality for all list items. The +// benefit to separating this logic from the item itself is that you can change +// the functionality of items without changing the actual items themselves. +// +// Note that if the delegate also implements help.KeyMap delegate-related +// help items will be added to the help view. +type ItemDelegate interface { + // Render renders the item's view. + Render(w io.Writer, m Model, index int, item Item) + + // Height is the height of the list item. + Height() int + + // Spacing is the size of the horizontal gap between list items in cells. + Spacing() int + + // Update is the update loop for items. All messages in the list's update + // loop will pass through here except when the user is setting a filter. + // Use this method to perform item-level updates appropriate to this + // delegate. + Update(msg tea.Msg, m *Model) tea.Cmd +} + +type filteredItem struct { + index int // index in the unfiltered list + item Item // item matched + matches []int // rune indices of matched items +} + +type filteredItems []filteredItem + +func (f filteredItems) items() []Item { + agg := make([]Item, len(f)) + for i, v := range f { + agg[i] = v.item + } + return agg +} + +// FilterMatchesMsg contains data about items matched during filtering. The +// message should be routed to Update for processing. +type FilterMatchesMsg []filteredItem + +// FilterFunc takes a term and a list of strings to search through +// (defined by Item#FilterValue). +// It should return a sorted list of ranks. +type FilterFunc func(string, []string) []Rank + +// Rank defines a rank for a given item. +type Rank struct { + // The index of the item in the original input. + Index int + // Indices of the actual word that were matched against the filter term. + MatchedIndexes []int +} + +// DefaultFilter uses the sahilm/fuzzy to filter through the list. +// This is set by default. +func DefaultFilter(term string, targets []string) []Rank { + ranks := fuzzy.Find(term, targets) + sort.Stable(ranks) + result := make([]Rank, len(ranks)) + for i, r := range ranks { + result[i] = Rank{ + Index: r.Index, + MatchedIndexes: r.MatchedIndexes, + } + } + return result +} + +// UnsortedFilter uses the sahilm/fuzzy to filter through the list. It does not +// sort the results. +func UnsortedFilter(term string, targets []string) []Rank { + ranks := fuzzy.FindNoSort(term, targets) + result := make([]Rank, len(ranks)) + for i, r := range ranks { + result[i] = Rank{ + Index: r.Index, + MatchedIndexes: r.MatchedIndexes, + } + } + return result +} + +type statusMessageTimeoutMsg struct{} + +// FilterState describes the current filtering state on the model. +type FilterState int + +// Possible filter states. +const ( + Unfiltered FilterState = iota // no filter set + Filtering // user is actively setting a filter + FilterApplied // a filter is applied and user is not editing filter +) + +// String returns a human-readable string of the current filter state. +func (f FilterState) String() string { + return [...]string{ + "unfiltered", + "filtering", + "filter applied", + }[f] +} + +// Model contains the state of this component. +type Model struct { + showTitle bool + showFilter bool + showStatusBar bool + showPagination bool + showHelp bool + filteringEnabled bool + + itemNameSingular string + itemNamePlural string + + Title string + Styles Styles + InfiniteScrolling bool + + // Key mappings for navigating the list. + KeyMap KeyMap + + // Filter is used to filter the list. + Filter FilterFunc + + disableQuitKeybindings bool + + // 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. + AdditionalShortHelpKeys func() []key.Binding + AdditionalFullHelpKeys func() []key.Binding + + spinner spinner.Model + showSpinner bool + width int + height int + Paginator paginator.Model + cursor int + Help help.Model + FilterInput textinput.Model + filterState FilterState + + // How long status messages should stay visible. By default this is + // 1 second. + StatusMessageLifetime time.Duration + + statusMessage string + statusMessageTimer *time.Timer + + // The master set of items we're working with. + items []Item + + // Filtered items we're currently displaying. Filtering, toggles and so on + // will alter this slice so we can show what is relevant. For that reason, + // this field should be considered ephemeral. + filteredItems filteredItems + + delegate ItemDelegate +} + +// New returns a new model with sensible defaults. +func New(items []Item, delegate ItemDelegate, width, height int) Model { + styles := DefaultStyles() + + sp := spinner.New() + sp.Spinner = spinner.Line + sp.Style = styles.Spinner + + filterInput := textinput.New() + filterInput.Prompt = "Filter: " + filterInput.PromptStyle = styles.FilterPrompt + filterInput.Cursor.Style = styles.FilterCursor + filterInput.CharLimit = 64 + filterInput.Focus() + + p := paginator.New() + p.Type = paginator.Dots + p.ActiveDot = styles.ActivePaginationDot.String() + p.InactiveDot = styles.InactivePaginationDot.String() + + m := Model{ + showTitle: true, + showFilter: true, + showStatusBar: true, + showPagination: true, + showHelp: true, + itemNameSingular: "item", + itemNamePlural: "items", + filteringEnabled: true, + KeyMap: DefaultKeyMap(), + Filter: DefaultFilter, + Styles: styles, + Title: "List", + FilterInput: filterInput, + StatusMessageLifetime: time.Second, + + width: width, + height: height, + delegate: delegate, + items: items, + Paginator: p, + spinner: sp, + Help: help.New(), + } + + m.updatePagination() + m.updateKeybindings() + return m +} + +// NewModel returns a new model with sensible defaults. +// +// Deprecated: use [New] instead. +var NewModel = New + +// SetFilteringEnabled enables or disables filtering. Note that this is different +// from ShowFilter, which merely hides or shows the input view. +func (m *Model) SetFilteringEnabled(v bool) { + m.filteringEnabled = v + if !v { + m.resetFiltering() + } + m.updateKeybindings() +} + +// FilteringEnabled returns whether or not filtering is enabled. +func (m Model) FilteringEnabled() bool { + return m.filteringEnabled +} + +// SetShowTitle shows or hides the title bar. +func (m *Model) SetShowTitle(v bool) { + m.showTitle = v + m.updatePagination() +} + +// SetFilterText explicitly sets the filter text without relying on user input. +// It also sets the filterState to a sane default of FilterApplied, but this +// can be changed with SetFilterState. +func (m *Model) SetFilterText(filter string) { + m.filterState = Filtering + m.FilterInput.SetValue(filter) + cmd := filterItems(*m) + msg := cmd() + fmm, _ := msg.(FilterMatchesMsg) + m.filteredItems = filteredItems(fmm) + m.filterState = FilterApplied + m.Paginator.Page = 0 + m.cursor = 0 + m.FilterInput.CursorEnd() + m.updatePagination() + m.updateKeybindings() +} + +// SetFilterState allows setting the filtering state manually. +func (m *Model) SetFilterState(state FilterState) { + m.Paginator.Page = 0 + m.cursor = 0 + m.filterState = state + m.FilterInput.CursorEnd() + m.FilterInput.Focus() + m.updateKeybindings() +} + +// ShowTitle returns whether or not the title bar is set to be rendered. +func (m Model) ShowTitle() bool { + return m.showTitle +} + +// SetShowFilter shows or hides the filter bar. Note that this does not disable +// filtering, it simply hides the built-in filter view. This allows you to +// use the FilterInput to render the filtering UI differently without having to +// re-implement filtering from scratch. +// +// To disable filtering entirely use EnableFiltering. +func (m *Model) SetShowFilter(v bool) { + m.showFilter = v + m.updatePagination() +} + +// ShowFilter returns whether or not the filter is set to be rendered. Note +// that this is separate from FilteringEnabled, so filtering can be hidden yet +// still invoked. This allows you to render filtering differently without +// having to re-implement it from scratch. +func (m Model) ShowFilter() bool { + return m.showFilter +} + +// SetShowStatusBar shows or hides the view that displays metadata about the +// list, such as item counts. +func (m *Model) SetShowStatusBar(v bool) { + m.showStatusBar = v + m.updatePagination() +} + +// ShowStatusBar returns whether or not the status bar is set to be rendered. +func (m Model) ShowStatusBar() bool { + return m.showStatusBar +} + +// SetStatusBarItemName defines a replacement for the item's identifier. +// Defaults to item/items. +func (m *Model) SetStatusBarItemName(singular, plural string) { + m.itemNameSingular = singular + m.itemNamePlural = plural +} + +// StatusBarItemName returns singular and plural status bar item names. +func (m Model) StatusBarItemName() (string, string) { + return m.itemNameSingular, m.itemNamePlural +} + +// SetShowPagination hides or shows the paginator. Note that pagination will +// still be active, it simply won't be displayed. +func (m *Model) SetShowPagination(v bool) { + m.showPagination = v + m.updatePagination() +} + +// ShowPagination returns whether the pagination is visible. +func (m *Model) ShowPagination() bool { + return m.showPagination +} + +// SetShowHelp shows or hides the help view. +func (m *Model) SetShowHelp(v bool) { + m.showHelp = v + m.updatePagination() +} + +// ShowHelp returns whether or not the help is set to be rendered. +func (m Model) ShowHelp() bool { + return m.showHelp +} + +// Items returns the items in the list. +func (m Model) Items() []Item { + return m.items +} + +// SetItems sets the items available in the list. This returns a command. +func (m *Model) SetItems(i []Item) tea.Cmd { + var cmd tea.Cmd + m.items = i + + if m.filterState != Unfiltered { + m.filteredItems = nil + cmd = filterItems(*m) + } + + m.updatePagination() + m.updateKeybindings() + return cmd +} + +// Select selects the given index of the list and goes to its respective page. +func (m *Model) Select(index int) { + m.Paginator.Page = index / m.Paginator.PerPage + m.cursor = index % m.Paginator.PerPage +} + +// ResetSelected resets the selected item to the first item in the first page of the list. +func (m *Model) ResetSelected() { + m.Select(0) +} + +// ResetFilter resets the current filtering state. +func (m *Model) ResetFilter() { + m.resetFiltering() +} + +// SetItem replaces an item at the given index. This returns a command. +func (m *Model) SetItem(index int, item Item) tea.Cmd { + var cmd tea.Cmd + m.items[index] = item + + if m.filterState != Unfiltered { + cmd = filterItems(*m) + } + + m.updatePagination() + return cmd +} + +// InsertItem inserts an item at the given index. If the index is out of the upper bound, +// the item will be appended. This returns a command. +func (m *Model) InsertItem(index int, item Item) tea.Cmd { + var cmd tea.Cmd + m.items = insertItemIntoSlice(m.items, item, index) + + if m.filterState != Unfiltered { + cmd = filterItems(*m) + } + + m.updatePagination() + m.updateKeybindings() + return cmd +} + +// RemoveItem removes an item at the given index. If the index is out of bounds +// this will be a no-op. O(n) complexity, which probably won't matter in the +// case of a TUI. +func (m *Model) RemoveItem(index int) { + m.items = removeItemFromSlice(m.items, index) + if m.filterState != Unfiltered { + m.filteredItems = removeFilterMatchFromSlice(m.filteredItems, index) + if len(m.filteredItems) == 0 { + m.resetFiltering() + } + } + m.updatePagination() +} + +// SetDelegate sets the item delegate. +func (m *Model) SetDelegate(d ItemDelegate) { + m.delegate = d + m.updatePagination() +} + +// VisibleItems returns the total items available to be shown. +func (m Model) VisibleItems() []Item { + if m.filterState != Unfiltered { + return m.filteredItems.items() + } + return m.items +} + +// SelectedItem returns the current selected item in the list. +func (m Model) SelectedItem() Item { + i := m.Index() + + items := m.VisibleItems() + if i < 0 || len(items) == 0 || len(items) <= i { + return nil + } + + return items[i] +} + +// MatchesForItem returns rune positions matched by the current filter, if any. +// Use this to style runes matched by the active filter. +// +// See DefaultItemView for a usage example. +func (m Model) MatchesForItem(index int) []int { + if m.filteredItems == nil || index >= len(m.filteredItems) { + return nil + } + return m.filteredItems[index].matches +} + +// Index returns the index of the currently selected item as it is stored in the +// filtered list of items. +// Using this value with SetItem() might be incorrect, consider using +// GlobalIndex() instead. +func (m Model) Index() int { + return m.Paginator.Page*m.Paginator.PerPage + m.cursor +} + +// GlobalIndex returns the index of the currently selected item as it is stored +// in the unfiltered list of items. This value can be used with SetItem(). +func (m Model) GlobalIndex() int { + index := m.Index() + + if m.filteredItems == nil || index >= len(m.filteredItems) { + return index + } + + return m.filteredItems[index].index +} + +// Cursor returns the index of the cursor on the current page. +func (m Model) Cursor() int { + return m.cursor +} + +// CursorUp moves the cursor up. This can also move the state to the previous +// page. +func (m *Model) CursorUp() { + m.cursor-- + + // If we're at the start, stop + if m.cursor < 0 && m.Paginator.Page == 0 { + // if infinite scrolling is enabled, go to the last item + if m.InfiniteScrolling { + m.Paginator.Page = m.Paginator.TotalPages - 1 + m.cursor = m.Paginator.ItemsOnPage(len(m.VisibleItems())) - 1 + return + } + + m.cursor = 0 + return + } + + // Move the cursor as normal + if m.cursor >= 0 { + return + } + + // Go to the previous page + m.Paginator.PrevPage() + m.cursor = m.Paginator.ItemsOnPage(len(m.VisibleItems())) - 1 +} + +// CursorDown moves the cursor down. This can also advance the state to the +// next page. +func (m *Model) CursorDown() { + itemsOnPage := m.Paginator.ItemsOnPage(len(m.VisibleItems())) + + m.cursor++ + + // If we're at the end, stop + if m.cursor < itemsOnPage { + return + } + + // Go to the next page + if !m.Paginator.OnLastPage() { + m.Paginator.NextPage() + m.cursor = 0 + return + } + + // During filtering the cursor position can exceed the number of + // itemsOnPage. It's more intuitive to start the cursor at the + // topmost position when moving it down in this scenario. + if m.cursor > itemsOnPage { + m.cursor = 0 + return + } + + m.cursor = itemsOnPage - 1 + + // if infinite scrolling is enabled, go to the first item + if m.InfiniteScrolling { + m.Paginator.Page = 0 + m.cursor = 0 + } +} + +// PrevPage moves to the previous page, if available. +func (m *Model) PrevPage() { + m.Paginator.PrevPage() +} + +// NextPage moves to the next page, if available. +func (m *Model) NextPage() { + m.Paginator.NextPage() +} + +// FilterState returns the current filter state. +func (m Model) FilterState() FilterState { + return m.filterState +} + +// FilterValue returns the current value of the filter. +func (m Model) FilterValue() string { + return m.FilterInput.Value() +} + +// SettingFilter returns whether or not the user is currently editing the +// filter value. It's purely a convenience method for the following: +// +// m.FilterState() == Filtering +// +// It's included here because it's a common thing to check for when +// implementing this component. +func (m Model) SettingFilter() bool { + return m.filterState == Filtering +} + +// IsFiltered returns whether or not the list is currently filtered. +// It's purely a convenience method for the following: +// +// m.FilterState() == FilterApplied +func (m Model) IsFiltered() bool { + return m.filterState == FilterApplied +} + +// Width returns the current width setting. +func (m Model) Width() int { + return m.width +} + +// Height returns the current height setting. +func (m Model) Height() int { + return m.height +} + +// SetSpinner allows to set the spinner style. +func (m *Model) SetSpinner(spinner spinner.Spinner) { + m.spinner.Spinner = spinner +} + +// ToggleSpinner toggles the spinner. Note that this also returns a command. +func (m *Model) ToggleSpinner() tea.Cmd { + if !m.showSpinner { + return m.StartSpinner() + } + m.StopSpinner() + return nil +} + +// StartSpinner starts the spinner. Note that this returns a command. +func (m *Model) StartSpinner() tea.Cmd { + m.showSpinner = true + return m.spinner.Tick +} + +// StopSpinner stops the spinner. +func (m *Model) StopSpinner() { + m.showSpinner = false +} + +// DisableQuitKeybindings is a helper for disabling the keybindings used for quitting, +// in case you want to handle this elsewhere in your application. +func (m *Model) DisableQuitKeybindings() { + m.disableQuitKeybindings = true + m.KeyMap.Quit.SetEnabled(false) + m.KeyMap.ForceQuit.SetEnabled(false) +} + +// NewStatusMessage sets a new status message, which will show for a limited +// amount of time. Note that this also returns a command. +func (m *Model) NewStatusMessage(s string) tea.Cmd { + m.statusMessage = s + if m.statusMessageTimer != nil { + m.statusMessageTimer.Stop() + } + + m.statusMessageTimer = time.NewTimer(m.StatusMessageLifetime) + + // Wait for timeout + return func() tea.Msg { + <-m.statusMessageTimer.C + return statusMessageTimeoutMsg{} + } +} + +// SetSize sets the width and height of this component. +func (m *Model) SetSize(width, height int) { + m.setSize(width, height) +} + +// SetWidth sets the width of this component. +func (m *Model) SetWidth(v int) { + m.setSize(v, m.height) +} + +// SetHeight sets the height of this component. +func (m *Model) SetHeight(v int) { + m.setSize(m.width, v) +} + +func (m *Model) setSize(width, height int) { + promptWidth := lipgloss.Width(m.Styles.Title.Render(m.FilterInput.Prompt)) + + m.width = width + m.height = height + m.Help.Width = width + m.FilterInput.Width = width - promptWidth - lipgloss.Width(m.spinnerView()) + m.updatePagination() +} + +func (m *Model) resetFiltering() { + if m.filterState == Unfiltered { + return + } + + m.filterState = Unfiltered + m.FilterInput.Reset() + m.filteredItems = nil + m.updatePagination() + m.updateKeybindings() +} + +func (m Model) itemsAsFilterItems() filteredItems { + fi := make([]filteredItem, len(m.items)) + for i, item := range m.items { + fi[i] = filteredItem{ + item: item, + } + } + return fi +} + +// Set keybindings according to the filter state. +func (m *Model) updateKeybindings() { + switch m.filterState { //nolint:exhaustive + case Filtering: + m.KeyMap.CursorUp.SetEnabled(false) + m.KeyMap.CursorDown.SetEnabled(false) + m.KeyMap.NextPage.SetEnabled(false) + m.KeyMap.PrevPage.SetEnabled(false) + m.KeyMap.GoToStart.SetEnabled(false) + m.KeyMap.GoToEnd.SetEnabled(false) + m.KeyMap.Filter.SetEnabled(false) + m.KeyMap.ClearFilter.SetEnabled(false) + m.KeyMap.CancelWhileFiltering.SetEnabled(true) + m.KeyMap.AcceptWhileFiltering.SetEnabled(m.FilterInput.Value() != "") + m.KeyMap.Quit.SetEnabled(false) + m.KeyMap.ShowFullHelp.SetEnabled(false) + m.KeyMap.CloseFullHelp.SetEnabled(false) + + default: + hasItems := len(m.items) != 0 + m.KeyMap.CursorUp.SetEnabled(hasItems) + m.KeyMap.CursorDown.SetEnabled(hasItems) + + hasPages := m.Paginator.TotalPages > 1 + m.KeyMap.NextPage.SetEnabled(hasPages) + m.KeyMap.PrevPage.SetEnabled(hasPages) + + m.KeyMap.GoToStart.SetEnabled(hasItems) + m.KeyMap.GoToEnd.SetEnabled(hasItems) + + m.KeyMap.Filter.SetEnabled(m.filteringEnabled && hasItems) + m.KeyMap.ClearFilter.SetEnabled(m.filterState == FilterApplied) + m.KeyMap.CancelWhileFiltering.SetEnabled(false) + m.KeyMap.AcceptWhileFiltering.SetEnabled(false) + m.KeyMap.Quit.SetEnabled(!m.disableQuitKeybindings) + + if m.Help.ShowAll { + m.KeyMap.ShowFullHelp.SetEnabled(true) + m.KeyMap.CloseFullHelp.SetEnabled(true) + } else { + minHelp := countEnabledBindings(m.FullHelp()) > 1 + m.KeyMap.ShowFullHelp.SetEnabled(minHelp) + m.KeyMap.CloseFullHelp.SetEnabled(minHelp) + } + } +} + +// Update pagination according to the amount of items for the current state. +func (m *Model) updatePagination() { + index := m.Index() + availHeight := m.height + + if m.showTitle || (m.showFilter && m.filteringEnabled) { + availHeight -= lipgloss.Height(m.titleView()) + } + if m.showStatusBar { + availHeight -= lipgloss.Height(m.statusView()) + } + if m.showPagination { + availHeight -= lipgloss.Height(m.paginationView()) + } + if m.showHelp { + availHeight -= lipgloss.Height(m.helpView()) + } + + m.Paginator.PerPage = max(1, availHeight/(m.delegate.Height()+m.delegate.Spacing())) + + if pages := len(m.VisibleItems()); pages < 1 { + m.Paginator.SetTotalPages(1) + } else { + m.Paginator.SetTotalPages(pages) + } + + // Restore index + m.Paginator.Page = index / m.Paginator.PerPage + m.cursor = index % m.Paginator.PerPage + + // Make sure the page stays in bounds + if m.Paginator.Page >= m.Paginator.TotalPages-1 { + m.Paginator.Page = max(0, m.Paginator.TotalPages-1) + } +} + +func (m *Model) hideStatusMessage() { + m.statusMessage = "" + if m.statusMessageTimer != nil { + m.statusMessageTimer.Stop() + } +} + +// Update is the Bubble Tea update loop. +func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { + var cmds []tea.Cmd + + switch msg := msg.(type) { + case tea.KeyMsg: + if key.Matches(msg, m.KeyMap.ForceQuit) { + return m, tea.Quit + } + + case FilterMatchesMsg: + m.filteredItems = filteredItems(msg) + return m, nil + + case spinner.TickMsg: + newSpinnerModel, cmd := m.spinner.Update(msg) + m.spinner = newSpinnerModel + if m.showSpinner { + cmds = append(cmds, cmd) + } + + case statusMessageTimeoutMsg: + m.hideStatusMessage() + } + + if m.filterState == Filtering { + cmds = append(cmds, m.handleFiltering(msg)) + } else { + cmds = append(cmds, m.handleBrowsing(msg)) + } + + return m, tea.Batch(cmds...) +} + +// Updates for when a user is browsing the list. +func (m *Model) handleBrowsing(msg tea.Msg) tea.Cmd { + var cmds []tea.Cmd + numItems := len(m.VisibleItems()) + + switch msg := msg.(type) { + case tea.KeyMsg: + switch { + // Note: we match clear filter before quit because, by default, they're + // both mapped to escape. + case key.Matches(msg, m.KeyMap.ClearFilter): + m.resetFiltering() + + case key.Matches(msg, m.KeyMap.Quit): + return tea.Quit + + case key.Matches(msg, m.KeyMap.CursorUp): + m.CursorUp() + + case key.Matches(msg, m.KeyMap.CursorDown): + m.CursorDown() + + case key.Matches(msg, m.KeyMap.PrevPage): + m.Paginator.PrevPage() + + case key.Matches(msg, m.KeyMap.NextPage): + m.Paginator.NextPage() + + case key.Matches(msg, m.KeyMap.GoToStart): + m.Paginator.Page = 0 + m.cursor = 0 + + case key.Matches(msg, m.KeyMap.GoToEnd): + m.Paginator.Page = m.Paginator.TotalPages - 1 + m.cursor = m.Paginator.ItemsOnPage(numItems) - 1 + + case key.Matches(msg, m.KeyMap.Filter): + m.hideStatusMessage() + if m.FilterInput.Value() == "" { + // Populate filter with all items only if the filter is empty. + m.filteredItems = m.itemsAsFilterItems() + } + m.Paginator.Page = 0 + m.cursor = 0 + m.filterState = Filtering + m.FilterInput.CursorEnd() + m.FilterInput.Focus() + m.updateKeybindings() + return textinput.Blink + + case key.Matches(msg, m.KeyMap.ShowFullHelp): + fallthrough + case key.Matches(msg, m.KeyMap.CloseFullHelp): + m.Help.ShowAll = !m.Help.ShowAll + m.updatePagination() + } + } + + cmd := m.delegate.Update(msg, m) + cmds = append(cmds, cmd) + + // Keep the index in bounds when paginating + itemsOnPage := m.Paginator.ItemsOnPage(len(m.VisibleItems())) + if m.cursor > itemsOnPage-1 { + m.cursor = max(0, itemsOnPage-1) + } + + return tea.Batch(cmds...) +} + +// Updates for when a user is in the filter editing interface. +func (m *Model) handleFiltering(msg tea.Msg) tea.Cmd { + var cmds []tea.Cmd + + // Handle keys + if msg, ok := msg.(tea.KeyMsg); ok { + switch { + case key.Matches(msg, m.KeyMap.CancelWhileFiltering): + m.resetFiltering() + m.KeyMap.Filter.SetEnabled(true) + m.KeyMap.ClearFilter.SetEnabled(false) + + case key.Matches(msg, m.KeyMap.AcceptWhileFiltering): + m.hideStatusMessage() + + if len(m.items) == 0 { + break + } + + h := m.VisibleItems() + + // If we've filtered down to nothing, clear the filter + if len(h) == 0 { + m.resetFiltering() + break + } + + m.FilterInput.Blur() + m.filterState = FilterApplied + m.updateKeybindings() + + if m.FilterInput.Value() == "" { + m.resetFiltering() + } + } + } + + // Update the filter text input component + newFilterInputModel, inputCmd := m.FilterInput.Update(msg) + filterChanged := m.FilterInput.Value() != newFilterInputModel.Value() + m.FilterInput = newFilterInputModel + cmds = append(cmds, inputCmd) + + // If the filtering input has changed, request updated filtering + if filterChanged { + cmds = append(cmds, filterItems(*m)) + m.KeyMap.AcceptWhileFiltering.SetEnabled(m.FilterInput.Value() != "") + } + + // Update pagination + m.updatePagination() + + return tea.Batch(cmds...) +} + +// ShortHelp returns bindings to show in the abbreviated help view. It's part +// of the help.KeyMap interface. +func (m Model) ShortHelp() []key.Binding { + kb := []key.Binding{ + m.KeyMap.CursorUp, + m.KeyMap.CursorDown, + } + + filtering := m.filterState == Filtering + + // If the delegate implements the help.KeyMap interface add the short help + // items to the short help after the cursor movement keys. + if !filtering { + if b, ok := m.delegate.(help.KeyMap); ok { + kb = append(kb, b.ShortHelp()...) + } + } + + kb = append(kb, + m.KeyMap.Filter, + m.KeyMap.ClearFilter, + m.KeyMap.AcceptWhileFiltering, + m.KeyMap.CancelWhileFiltering, + ) + + if !filtering && m.AdditionalShortHelpKeys != nil { + kb = append(kb, m.AdditionalShortHelpKeys()...) + } + + return append(kb, + m.KeyMap.Quit, + m.KeyMap.ShowFullHelp, + ) +} + +// FullHelp returns bindings to show the full help view. It's part of the +// help.KeyMap interface. +func (m Model) FullHelp() [][]key.Binding { + kb := [][]key.Binding{{ + m.KeyMap.CursorUp, + m.KeyMap.CursorDown, + m.KeyMap.NextPage, + m.KeyMap.PrevPage, + m.KeyMap.GoToStart, + m.KeyMap.GoToEnd, + }} + + filtering := m.filterState == Filtering + + // If the delegate implements the help.KeyMap interface add full help + // keybindings to a special section of the full help. + if !filtering { + if b, ok := m.delegate.(help.KeyMap); ok { + kb = append(kb, b.FullHelp()...) + } + } + + listLevelBindings := []key.Binding{ + m.KeyMap.Filter, + m.KeyMap.ClearFilter, + m.KeyMap.AcceptWhileFiltering, + m.KeyMap.CancelWhileFiltering, + } + + if !filtering && m.AdditionalFullHelpKeys != nil { + listLevelBindings = append(listLevelBindings, m.AdditionalFullHelpKeys()...) + } + + return append(kb, + listLevelBindings, + []key.Binding{ + m.KeyMap.Quit, + m.KeyMap.CloseFullHelp, + }) +} + +// View renders the component. +func (m Model) View() string { + var ( + sections []string + availHeight = m.height + ) + + if m.showTitle || (m.showFilter && m.filteringEnabled) { + v := m.titleView() + sections = append(sections, v) + availHeight -= lipgloss.Height(v) + } + + if m.showStatusBar { + v := m.statusView() + sections = append(sections, v) + availHeight -= lipgloss.Height(v) + } + + var pagination string + if m.showPagination { + pagination = m.paginationView() + availHeight -= lipgloss.Height(pagination) + } + + var help string + if m.showHelp { + help = m.helpView() + availHeight -= lipgloss.Height(help) + } + + content := lipgloss.NewStyle().Height(availHeight).Render(m.populatedView()) + sections = append(sections, content) + + if m.showPagination { + sections = append(sections, pagination) + } + + if m.showHelp { + sections = append(sections, help) + } + + return lipgloss.JoinVertical(lipgloss.Left, sections...) +} + +func (m Model) titleView() string { + var ( + view string + titleBarStyle = m.Styles.TitleBar + + // We need to account for the size of the spinner, even if we don't + // render it, to reserve some space for it should we turn it on later. + spinnerView = m.spinnerView() + spinnerWidth = lipgloss.Width(spinnerView) + spinnerLeftGap = " " + spinnerOnLeft = titleBarStyle.GetPaddingLeft() >= spinnerWidth+lipgloss.Width(spinnerLeftGap) && m.showSpinner + ) + + // If the filter's showing, draw that. Otherwise draw the title. + if m.showFilter && m.filterState == Filtering { + view += m.FilterInput.View() + } else if m.showTitle { + if m.showSpinner && spinnerOnLeft { + view += spinnerView + spinnerLeftGap + titleBarGap := titleBarStyle.GetPaddingLeft() + titleBarStyle = titleBarStyle.PaddingLeft(titleBarGap - spinnerWidth - lipgloss.Width(spinnerLeftGap)) + } + + view += m.Styles.Title.Render(m.Title) + + // Status message + if m.filterState != Filtering { + view += " " + m.statusMessage + view = ansi.Truncate(view, m.width-spinnerWidth, ellipsis) + } + } + + // Spinner + if m.showSpinner && !spinnerOnLeft { + // Place spinner on the right + availSpace := m.width - lipgloss.Width(m.Styles.TitleBar.Render(view)) + if availSpace > spinnerWidth { + view += strings.Repeat(" ", availSpace-spinnerWidth) + view += spinnerView + } + } + + if len(view) > 0 { + return titleBarStyle.Render(view) + } + return view +} + +func (m Model) statusView() string { + var status string + + totalItems := len(m.items) + visibleItems := len(m.VisibleItems()) + + var itemName string + if visibleItems != 1 { + itemName = m.itemNamePlural + } else { + itemName = m.itemNameSingular + } + + itemsDisplay := fmt.Sprintf("%d %s", visibleItems, itemName) + + if m.filterState == Filtering { //nolint:nestif + // Filter results + if visibleItems == 0 { + status = m.Styles.StatusEmpty.Render("Nothing matched") + } else { + status = itemsDisplay + } + } else if len(m.items) == 0 { + // Not filtering: no items. + status = m.Styles.StatusEmpty.Render("No " + m.itemNamePlural) + } else { + // Normal + filtered := m.FilterState() == FilterApplied + + if filtered { + f := strings.TrimSpace(m.FilterInput.Value()) + f = ansi.Truncate(f, 10, "…") //nolint:mnd + status += fmt.Sprintf("“%s” ", f) + } + + status += itemsDisplay + } + + numFiltered := totalItems - visibleItems + if numFiltered > 0 { + status += m.Styles.DividerDot.String() + status += m.Styles.StatusBarFilterCount.Render(fmt.Sprintf("%d filtered", numFiltered)) + } + + return m.Styles.StatusBar.Render(status) +} + +func (m Model) paginationView() string { + if m.Paginator.TotalPages < 2 { //nolint:mnd + return "" + } + + s := m.Paginator.View() + + // If the dot pagination is wider than the width of the window + // use the arabic paginator. + if ansi.StringWidth(s) > m.width { + m.Paginator.Type = paginator.Arabic + s = m.Styles.ArabicPagination.Render(m.Paginator.View()) + } + + style := m.Styles.PaginationStyle + if m.delegate.Spacing() == 0 && style.GetMarginTop() == 0 { + style = style.MarginTop(1) + } + + return style.Render(s) +} + +func (m Model) populatedView() string { + items := m.VisibleItems() + + var b strings.Builder + + // Empty states + if len(items) == 0 { + if m.filterState == Filtering { + return "" + } + return m.Styles.NoItems.Render("No " + m.itemNamePlural + ".") + } + + if len(items) > 0 { + start, end := m.Paginator.GetSliceBounds(len(items)) + docs := items[start:end] + + for i, item := range docs { + m.delegate.Render(&b, m, i+start, item) + if i != len(docs)-1 { + fmt.Fprint(&b, strings.Repeat("\n", m.delegate.Spacing()+1)) + } + } + } + + // If there aren't enough items to fill up this page (always the last page) + // then we need to add some newlines to fill up the space where items would + // have been. + itemsOnPage := m.Paginator.ItemsOnPage(len(items)) + if itemsOnPage < m.Paginator.PerPage { + n := (m.Paginator.PerPage - itemsOnPage) * (m.delegate.Height() + m.delegate.Spacing()) + if len(items) == 0 { + n -= m.delegate.Height() - 1 + } + fmt.Fprint(&b, strings.Repeat("\n", n)) + } + + return b.String() +} + +func (m Model) helpView() string { + return m.Styles.HelpStyle.Render(m.Help.View(m)) +} + +func (m Model) spinnerView() string { + return m.spinner.View() +} + +func filterItems(m Model) tea.Cmd { + return func() tea.Msg { + if m.FilterInput.Value() == "" || m.filterState == Unfiltered { + return FilterMatchesMsg(m.itemsAsFilterItems()) // return nothing + } + + items := m.items + targets := make([]string, len(items)) + + for i, t := range items { + targets[i] = t.FilterValue() + } + + filterMatches := []filteredItem{} + for _, r := range m.Filter(m.FilterInput.Value(), targets) { + filterMatches = append(filterMatches, filteredItem{ + index: r.Index, + item: items[r.Index], + matches: r.MatchedIndexes, + }) + } + + return FilterMatchesMsg(filterMatches) + } +} + +func insertItemIntoSlice(items []Item, item Item, index int) []Item { + if items == nil { + return []Item{item} + } + if index >= len(items) { + return append(items, item) + } + + index = max(0, index) + + items = append(items, nil) + copy(items[index+1:], items[index:]) + items[index] = item + return items +} + +// Remove an item from a slice of items at the given index. This runs in O(n). +func removeItemFromSlice(i []Item, index int) []Item { + if index >= len(i) { + return i // noop + } + copy(i[index:], i[index+1:]) + i[len(i)-1] = nil + return i[:len(i)-1] +} + +func removeFilterMatchFromSlice(i []filteredItem, index int) []filteredItem { + if index >= len(i) { + return i // noop + } + copy(i[index:], i[index+1:]) + i[len(i)-1] = filteredItem{} + return i[:len(i)-1] +} + +func countEnabledBindings(groups [][]key.Binding) (agg int) { + for _, group := range groups { + for _, kb := range group { + if kb.Enabled() { + agg++ + } + } + } + return agg +} diff --git a/vendor/github.com/charmbracelet/bubbles/list/style.go b/vendor/github.com/charmbracelet/bubbles/list/style.go new file mode 100644 index 00000000..e663c07b --- /dev/null +++ b/vendor/github.com/charmbracelet/bubbles/list/style.go @@ -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 +} diff --git a/vendor/github.com/charmbracelet/bubbles/paginator/paginator.go b/vendor/github.com/charmbracelet/bubbles/paginator/paginator.go new file mode 100644 index 00000000..12e4e934 --- /dev/null +++ b/vendor/github.com/charmbracelet/bubbles/paginator/paginator.go @@ -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) +} diff --git a/vendor/github.com/evertras/bubble-table/table/border.go b/vendor/github.com/evertras/bubble-table/table/border.go deleted file mode 100644 index 756d4c81..00000000 --- a/vendor/github.com/evertras/bubble-table/table/border.go +++ /dev/null @@ -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 -} diff --git a/vendor/github.com/evertras/bubble-table/table/calc.go b/vendor/github.com/evertras/bubble-table/table/calc.go deleted file mode 100644 index 21b7a7d7..00000000 --- a/vendor/github.com/evertras/bubble-table/table/calc.go +++ /dev/null @@ -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) -} diff --git a/vendor/github.com/evertras/bubble-table/table/cell.go b/vendor/github.com/evertras/bubble-table/table/cell.go deleted file mode 100644 index ac9e5c40..00000000 --- a/vendor/github.com/evertras/bubble-table/table/cell.go +++ /dev/null @@ -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, - } -} diff --git a/vendor/github.com/evertras/bubble-table/table/column.go b/vendor/github.com/evertras/bubble-table/table/column.go deleted file mode 100644 index 44714755..00000000 --- a/vendor/github.com/evertras/bubble-table/table/column.go +++ /dev/null @@ -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 -} diff --git a/vendor/github.com/evertras/bubble-table/table/data.go b/vendor/github.com/evertras/bubble-table/table/data.go deleted file mode 100644 index 901a4f4b..00000000 --- a/vendor/github.com/evertras/bubble-table/table/data.go +++ /dev/null @@ -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 -} diff --git a/vendor/github.com/evertras/bubble-table/table/dimensions.go b/vendor/github.com/evertras/bubble-table/table/dimensions.go deleted file mode 100644 index 38074616..00000000 --- a/vendor/github.com/evertras/bubble-table/table/dimensions.go +++ /dev/null @@ -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 -} diff --git a/vendor/github.com/evertras/bubble-table/table/doc.go b/vendor/github.com/evertras/bubble-table/table/doc.go deleted file mode 100644 index 554944af..00000000 --- a/vendor/github.com/evertras/bubble-table/table/doc.go +++ /dev/null @@ -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 diff --git a/vendor/github.com/evertras/bubble-table/table/events.go b/vendor/github.com/evertras/bubble-table/table/events.go deleted file mode 100644 index b519623d..00000000 --- a/vendor/github.com/evertras/bubble-table/table/events.go +++ /dev/null @@ -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{} diff --git a/vendor/github.com/evertras/bubble-table/table/filter.go b/vendor/github.com/evertras/bubble-table/table/filter.go deleted file mode 100644 index 765e5b33..00000000 --- a/vendor/github.com/evertras/bubble-table/table/filter.go +++ /dev/null @@ -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) -} diff --git a/vendor/github.com/evertras/bubble-table/table/footer.go b/vendor/github.com/evertras/bubble-table/table/footer.go deleted file mode 100644 index c68709ce..00000000 --- a/vendor/github.com/evertras/bubble-table/table/footer.go +++ /dev/null @@ -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) -} diff --git a/vendor/github.com/evertras/bubble-table/table/header.go b/vendor/github.com/evertras/bubble-table/table/header.go deleted file mode 100644 index fdd5ac0b..00000000 --- a/vendor/github.com/evertras/bubble-table/table/header.go +++ /dev/null @@ -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 -} diff --git a/vendor/github.com/evertras/bubble-table/table/keys.go b/vendor/github.com/evertras/bubble-table/table/keys.go deleted file mode 100644 index fe2eccc3..00000000 --- a/vendor/github.com/evertras/bubble-table/table/keys.go +++ /dev/null @@ -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("/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 -} diff --git a/vendor/github.com/evertras/bubble-table/table/model.go b/vendor/github.com/evertras/bubble-table/table/model.go deleted file mode 100644 index 33e1458c..00000000 --- a/vendor/github.com/evertras/bubble-table/table/model.go +++ /dev/null @@ -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 -} diff --git a/vendor/github.com/evertras/bubble-table/table/options.go b/vendor/github.com/evertras/bubble-table/table/options.go deleted file mode 100644 index c35fc1e7..00000000 --- a/vendor/github.com/evertras/bubble-table/table/options.go +++ /dev/null @@ -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 -} diff --git a/vendor/github.com/evertras/bubble-table/table/overflow.go b/vendor/github.com/evertras/bubble-table/table/overflow.go deleted file mode 100644 index 19c5b0aa..00000000 --- a/vendor/github.com/evertras/bubble-table/table/overflow.go +++ /dev/null @@ -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) -} diff --git a/vendor/github.com/evertras/bubble-table/table/pagination.go b/vendor/github.com/evertras/bubble-table/table/pagination.go deleted file mode 100644 index 6fce9b51..00000000 --- a/vendor/github.com/evertras/bubble-table/table/pagination.go +++ /dev/null @@ -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 -} diff --git a/vendor/github.com/evertras/bubble-table/table/query.go b/vendor/github.com/evertras/bubble-table/table/query.go deleted file mode 100644 index 79da50f0..00000000 --- a/vendor/github.com/evertras/bubble-table/table/query.go +++ /dev/null @@ -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 -} diff --git a/vendor/github.com/evertras/bubble-table/table/row.go b/vendor/github.com/evertras/bubble-table/table/row.go deleted file mode 100644 index b9a5d6bb..00000000 --- a/vendor/github.com/evertras/bubble-table/table/row.go +++ /dev/null @@ -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 -} diff --git a/vendor/github.com/evertras/bubble-table/table/scrolling.go b/vendor/github.com/evertras/bubble-table/table/scrolling.go deleted file mode 100644 index 3ee3256c..00000000 --- a/vendor/github.com/evertras/bubble-table/table/scrolling.go +++ /dev/null @@ -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 - } - } -} diff --git a/vendor/github.com/evertras/bubble-table/table/sort.go b/vendor/github.com/evertras/bubble-table/table/sort.go deleted file mode 100644 index 9a282cbd..00000000 --- a/vendor/github.com/evertras/bubble-table/table/sort.go +++ /dev/null @@ -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 -} diff --git a/vendor/github.com/evertras/bubble-table/table/strlimit.go b/vendor/github.com/evertras/bubble-table/table/strlimit.go deleted file mode 100644 index 9889d831..00000000 --- a/vendor/github.com/evertras/bubble-table/table/strlimit.go +++ /dev/null @@ -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 -} diff --git a/vendor/github.com/evertras/bubble-table/table/update.go b/vendor/github.com/evertras/bubble-table/table/update.go deleted file mode 100644 index 198a57d2..00000000 --- a/vendor/github.com/evertras/bubble-table/table/update.go +++ /dev/null @@ -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 -} diff --git a/vendor/github.com/evertras/bubble-table/table/view.go b/vendor/github.com/evertras/bubble-table/table/view.go deleted file mode 100644 index 7be99c23..00000000 --- a/vendor/github.com/evertras/bubble-table/table/view.go +++ /dev/null @@ -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() -} diff --git a/vendor/github.com/muesli/reflow/LICENSE b/vendor/github.com/muesli/reflow/LICENSE deleted file mode 100644 index 8532c45c..00000000 --- a/vendor/github.com/muesli/reflow/LICENSE +++ /dev/null @@ -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. diff --git a/vendor/github.com/muesli/reflow/ansi/ansi.go b/vendor/github.com/muesli/reflow/ansi/ansi.go deleted file mode 100644 index f3d0700a..00000000 --- a/vendor/github.com/muesli/reflow/ansi/ansi.go +++ /dev/null @@ -1,7 +0,0 @@ -package ansi - -const Marker = '\x1B' - -func IsTerminator(c rune) bool { - return (c >= 0x40 && c <= 0x5a) || (c >= 0x61 && c <= 0x7a) -} diff --git a/vendor/github.com/muesli/reflow/ansi/buffer.go b/vendor/github.com/muesli/reflow/ansi/buffer.go deleted file mode 100644 index 471bcaf7..00000000 --- a/vendor/github.com/muesli/reflow/ansi/buffer.go +++ /dev/null @@ -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 -} diff --git a/vendor/github.com/muesli/reflow/ansi/writer.go b/vendor/github.com/muesli/reflow/ansi/writer.go deleted file mode 100644 index a6aaa1ec..00000000 --- a/vendor/github.com/muesli/reflow/ansi/writer.go +++ /dev/null @@ -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()) -} diff --git a/vendor/github.com/muesli/reflow/truncate/truncate.go b/vendor/github.com/muesli/reflow/truncate/truncate.go deleted file mode 100644 index 5aab5f89..00000000 --- a/vendor/github.com/muesli/reflow/truncate/truncate.go +++ /dev/null @@ -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() -} diff --git a/vendor/github.com/muesli/reflow/wordwrap/wordwrap.go b/vendor/github.com/muesli/reflow/wordwrap/wordwrap.go deleted file mode 100644 index 488fb210..00000000 --- a/vendor/github.com/muesli/reflow/wordwrap/wordwrap.go +++ /dev/null @@ -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() -} diff --git a/vendor/github.com/sahilm/fuzzy/.editorconfig b/vendor/github.com/sahilm/fuzzy/.editorconfig new file mode 100644 index 00000000..8a8f6a57 --- /dev/null +++ b/vendor/github.com/sahilm/fuzzy/.editorconfig @@ -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 diff --git a/vendor/github.com/sahilm/fuzzy/.gitignore b/vendor/github.com/sahilm/fuzzy/.gitignore new file mode 100644 index 00000000..d6c59ee8 --- /dev/null +++ b/vendor/github.com/sahilm/fuzzy/.gitignore @@ -0,0 +1,2 @@ +vendor/ +coverage/ diff --git a/vendor/github.com/sahilm/fuzzy/.travis.yml b/vendor/github.com/sahilm/fuzzy/.travis.yml new file mode 100644 index 00000000..f77acde7 --- /dev/null +++ b/vendor/github.com/sahilm/fuzzy/.travis.yml @@ -0,0 +1,8 @@ +arch: + - amd64 + - ppc64le +language: go +go: + - 1.x +script: + - make diff --git a/vendor/github.com/sahilm/fuzzy/CONTRIBUTING.md b/vendor/github.com/sahilm/fuzzy/CONTRIBUTING.md new file mode 100644 index 00000000..7068ce13 --- /dev/null +++ b/vendor/github.com/sahilm/fuzzy/CONTRIBUTING.md @@ -0,0 +1 @@ +Everyone is welcome to contribute. Please send me a pull request or file an issue. I promise to respond promptly. diff --git a/vendor/github.com/sahilm/fuzzy/Gopkg.lock b/vendor/github.com/sahilm/fuzzy/Gopkg.lock new file mode 100644 index 00000000..6e3a7fe5 --- /dev/null +++ b/vendor/github.com/sahilm/fuzzy/Gopkg.lock @@ -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 diff --git a/vendor/github.com/sahilm/fuzzy/Gopkg.toml b/vendor/github.com/sahilm/fuzzy/Gopkg.toml new file mode 100644 index 00000000..8f96b112 --- /dev/null +++ b/vendor/github.com/sahilm/fuzzy/Gopkg.toml @@ -0,0 +1,4 @@ +# Test dependency +[[constraint]] + branch = "master" + name = "github.com/kylelemons/godebug" diff --git a/vendor/github.com/evertras/bubble-table/LICENSE b/vendor/github.com/sahilm/fuzzy/LICENSE similarity index 94% rename from vendor/github.com/evertras/bubble-table/LICENSE rename to vendor/github.com/sahilm/fuzzy/LICENSE index 0a30e64a..f848719e 100644 --- a/vendor/github.com/evertras/bubble-table/LICENSE +++ b/vendor/github.com/sahilm/fuzzy/LICENSE @@ -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 diff --git a/vendor/github.com/sahilm/fuzzy/Makefile b/vendor/github.com/sahilm/fuzzy/Makefile new file mode 100644 index 00000000..7fa2be4e --- /dev/null +++ b/vendor/github.com/sahilm/fuzzy/Makefile @@ -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 diff --git a/vendor/github.com/sahilm/fuzzy/README.md b/vendor/github.com/sahilm/fuzzy/README.md new file mode 100644 index 00000000..ea7bf22b --- /dev/null +++ b/vendor/github.com/sahilm/fuzzy/README.md @@ -0,0 +1,186 @@ +gopher looking for stuff gopher found stuff + +# fuzzy +[![Build Status](https://travis-ci.org/sahilm/fuzzy.svg?branch=master)](https://travis-ci.org/sahilm/fuzzy) +[![Documentation](https://godoc.org/github.com/sahilm/fuzzy?status.svg)](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. + +![demo](assets/demo.gif) + +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. + diff --git a/vendor/github.com/sahilm/fuzzy/fuzzy.go b/vendor/github.com/sahilm/fuzzy/fuzzy.go new file mode 100644 index 00000000..5125821f --- /dev/null +++ b/vendor/github.com/sahilm/fuzzy/fuzzy.go @@ -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 +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 72c4be64..8ed8b57f 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -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