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  }