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 }