-
-
Notifications
You must be signed in to change notification settings - Fork 177
/
prompt.go
144 lines (126 loc) · 3.53 KB
/
prompt.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
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) (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)
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
}