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