github.com/supabase/cli@v1.168.1/internal/utils/prompt.go (about)

     1  package utils
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"io"
     7  	"strings"
     8  
     9  	"github.com/charmbracelet/bubbles/list"
    10  	tea "github.com/charmbracelet/bubbletea"
    11  	"github.com/charmbracelet/lipgloss"
    12  	"github.com/go-errors/errors"
    13  )
    14  
    15  var (
    16  	titleStyle        = lipgloss.NewStyle().MarginLeft(2)
    17  	itemStyle         = lipgloss.NewStyle().PaddingLeft(4)
    18  	selectedItemStyle = lipgloss.NewStyle().PaddingLeft(2).Foreground(lipgloss.Color("170"))
    19  	paginationStyle   = list.DefaultStyles().PaginationStyle.PaddingLeft(4)
    20  	helpStyle         = list.DefaultStyles().HelpStyle.PaddingLeft(4).PaddingBottom(1)
    21  )
    22  
    23  // PromptItem is exposed as prompt input, empty summary + details will be excluded.
    24  type PromptItem struct {
    25  	Summary string
    26  	Details string
    27  	Index   int
    28  }
    29  
    30  func (i PromptItem) Title() string       { return i.Summary }
    31  func (i PromptItem) Description() string { return i.Details }
    32  func (i PromptItem) FilterValue() string { return i.Summary + " " + i.Details }
    33  
    34  // Item delegate is used to finetune the list item renderer.
    35  type itemDelegate struct{}
    36  
    37  func (d itemDelegate) Height() int                               { return 1 }
    38  func (d itemDelegate) Spacing() int                              { return 0 }
    39  func (d itemDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd { return nil }
    40  func (d itemDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) {
    41  	i, ok := listItem.(PromptItem)
    42  	if !ok {
    43  		return
    44  	}
    45  
    46  	str := fmt.Sprintf("%d. %s", index+1, i.Summary)
    47  	if i.Details != "" {
    48  		str += fmt.Sprintf(" [%s]", i.Details)
    49  	}
    50  
    51  	fn := itemStyle.Render
    52  	if index == m.Index() {
    53  		fn = func(s ...string) string {
    54  			items := append([]string{"> "}, s...)
    55  			return selectedItemStyle.Render(items...)
    56  		}
    57  	}
    58  
    59  	fmt.Fprint(w, fn(str))
    60  }
    61  
    62  // Model is used to store state of user choices.
    63  type model struct {
    64  	cancel context.CancelFunc
    65  	list   list.Model
    66  	choice PromptItem
    67  }
    68  
    69  func (m model) Init() tea.Cmd {
    70  	return nil
    71  }
    72  
    73  func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    74  	switch msg := msg.(type) {
    75  	case tea.WindowSizeMsg:
    76  		m.list.SetWidth(msg.Width)
    77  		return m, nil
    78  
    79  	case tea.KeyMsg:
    80  		switch msg.Type {
    81  		case tea.KeyCtrlC:
    82  			m.cancel()
    83  			return m, tea.Quit
    84  
    85  		case tea.KeyEnter:
    86  			if choice, ok := m.list.SelectedItem().(PromptItem); ok {
    87  				m.choice = choice
    88  			}
    89  			return m, tea.Quit
    90  		}
    91  	}
    92  
    93  	var cmd tea.Cmd
    94  	m.list, cmd = m.list.Update(msg)
    95  	return m, cmd
    96  }
    97  
    98  func (m model) View() string {
    99  	if m.choice.Summary != "" {
   100  		return ""
   101  	}
   102  	return "\n" + m.list.View()
   103  }
   104  
   105  // Prompt user to choose from a list of items, returns the chosen index.
   106  func PromptChoice(ctx context.Context, title string, items []PromptItem) (PromptItem, error) {
   107  	// Create list items
   108  	var listItems []list.Item
   109  	for _, v := range items {
   110  		if strings.TrimSpace(v.FilterValue()) == "" {
   111  			continue
   112  		}
   113  		listItems = append(listItems, v)
   114  	}
   115  	// Create list model
   116  	height := len(listItems) * 4
   117  	if height > 14 {
   118  		height = 14
   119  	}
   120  	l := list.New(listItems, itemDelegate{}, 0, height)
   121  	l.Title = title
   122  	l.SetShowStatusBar(false)
   123  	l.Styles.Title = titleStyle
   124  	l.Styles.PaginationStyle = paginationStyle
   125  	l.Styles.HelpStyle = helpStyle
   126  	// Create our model
   127  	ctx, cancel := context.WithCancel(ctx)
   128  	initial := model{cancel: cancel, list: l}
   129  	prog := tea.NewProgram(initial)
   130  	state, err := prog.Run()
   131  	if err != nil {
   132  		return initial.choice, errors.Errorf("failed to prompt choice: %w", err)
   133  	}
   134  	if ctx.Err() != nil {
   135  		return initial.choice, ctx.Err()
   136  	}
   137  	if m, ok := state.(model); ok {
   138  		if m.choice == initial.choice {
   139  			return initial.choice, errors.New("user aborted")
   140  		}
   141  		return m.choice, nil
   142  	}
   143  	return initial.choice, err
   144  }