supabase-cli/internal/utils/prompt.go

145 lines
3.6 KiB
Go

package utils
import (
"context"
"fmt"
"io"
"strings"
"github.com/charmbracelet/bubbles/list"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/go-errors/errors"
)
var (
titleStyle = lipgloss.NewStyle().MarginLeft(2)
itemStyle = lipgloss.NewStyle().PaddingLeft(4)
selectedItemStyle = lipgloss.NewStyle().PaddingLeft(2).Foreground(lipgloss.Color("170"))
paginationStyle = list.DefaultStyles().PaginationStyle.PaddingLeft(4)
helpStyle = list.DefaultStyles().HelpStyle.PaddingLeft(4).PaddingBottom(1)
)
// PromptItem is exposed as prompt input, empty summary + details will be excluded.
type PromptItem struct {
Summary string
Details string
Index int
}
func (i PromptItem) Title() string { return i.Summary }
func (i PromptItem) Description() string { return i.Details }
func (i PromptItem) FilterValue() string { return i.Summary + " " + i.Details }
// Item delegate is used to finetune the list item renderer.
type itemDelegate struct{}
func (d itemDelegate) Height() int { return 1 }
func (d itemDelegate) Spacing() int { return 0 }
func (d itemDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd { return nil }
func (d itemDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) {
i, ok := listItem.(PromptItem)
if !ok {
return
}
str := fmt.Sprintf("%d. %s", index+1, i.Summary)
if i.Details != "" {
str += fmt.Sprintf(" [%s]", i.Details)
}
fn := itemStyle.Render
if index == m.Index() {
fn = func(s ...string) string {
items := append([]string{"> "}, s...)
return selectedItemStyle.Render(items...)
}
}
fmt.Fprint(w, fn(str))
}
// Model is used to store state of user choices.
type model struct {
cancel context.CancelFunc
list list.Model
choice PromptItem
}
func (m model) Init() tea.Cmd {
return nil
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
m.list.SetWidth(msg.Width)
return m, nil
case tea.KeyMsg:
switch msg.Type {
case tea.KeyCtrlC:
m.cancel()
return m, tea.Quit
case tea.KeyEnter:
if choice, ok := m.list.SelectedItem().(PromptItem); ok {
m.choice = choice
}
return m, tea.Quit
}
}
var cmd tea.Cmd
m.list, cmd = m.list.Update(msg)
return m, cmd
}
func (m model) View() string {
if m.choice.Summary != "" {
return ""
}
return "\n" + m.list.View()
}
// Prompt user to choose from a list of items, returns the chosen index.
func PromptChoice(ctx context.Context, title string, items []PromptItem, opts ...tea.ProgramOption) (PromptItem, error) {
// Create list items
var listItems []list.Item
for _, v := range items {
if strings.TrimSpace(v.FilterValue()) == "" {
continue
}
listItems = append(listItems, v)
}
// Create list model
height := len(listItems) * 4
if height > 14 {
height = 14
}
l := list.New(listItems, itemDelegate{}, 0, height)
l.Title = title
l.SetShowStatusBar(false)
l.Styles.Title = titleStyle
l.Styles.PaginationStyle = paginationStyle
l.Styles.HelpStyle = helpStyle
// Create our model
ctx, cancel := context.WithCancel(ctx)
initial := model{cancel: cancel, list: l}
prog := tea.NewProgram(initial, opts...)
state, err := prog.Run()
if err != nil {
return initial.choice, errors.Errorf("failed to prompt choice: %w", err)
}
if ctx.Err() != nil {
return initial.choice, ctx.Err()
}
if m, ok := state.(model); ok {
if m.choice == initial.choice {
return initial.choice, errors.New("user aborted")
}
return m.choice, nil
}
return initial.choice, err
}