145 lines
3.6 KiB
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
|
|
}
|