tui/picker.go (230 lines of code) (raw):
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package tui
import (
"fmt"
"io"
"strings"
"github.com/charmbracelet/bubbles/list"
"github.com/charmbracelet/bubbles/spinner"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
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.(item)
if !ok {
return
}
str := fmt.Sprintf("%2d. %-50s", index+1, i.label)
fn := itemStyle.Render
if index == m.Index() {
color := selectedItemStyle.background.code()
fn = func(s string) string {
defaultItemStyle := lipgloss.NewStyle().Bold(true)
return selectedItemStyle.Render(color + "> " + defaultItemStyle.Render(s) + clear)
}
}
fmt.Fprint(w, fn(str))
}
type item struct {
label, value string
}
func (i item) FilterValue() string { return i.value }
type picker struct {
dynamicPage
list list.Model
target string
defaultValue string
}
func newPicker(listLabel, spinnerLabel, key, defaultValue string, preProcessor tea.Cmd) picker {
p := picker{}
l := list.New([]list.Item{}, itemDelegate{}, 0, 19)
l.Title = listLabel
l.Styles.Title = titleStyle.style
l.Styles.PaginationStyle = paginationStyle
l.Styles.HelpStyle = helpStyle
p.list = l
p.showProgress = true
p.defaultValue = defaultValue
p.preProcessor = preProcessor
p.key = key
p.state = "idle"
if preProcessor != nil {
p.state = "querying"
}
p.spinnerLabel = spinnerLabel
s := spinner.New()
s.Spinner = spinnerType
p.spinner = s
return p
}
func positionDefault(items []list.Item, defaultValue string) ([]list.Item, int) {
selectedIndex := 0
if defaultValue == "" {
return items, selectedIndex
}
defaultIndex := 0
newItems := []list.Item{}
defaultItem := item{}
createItem := item{}
returnItems := []list.Item{}
for i, v := range items {
item := v.(item)
if item.value == defaultValue || item.label == defaultValue || defaultValue == item.value+"|"+item.label {
defaultItem = item
text := defaultItem.label + " (Default Value)"
defaultItem.label = text
items[i] = defaultItem
defaultIndex = i
continue
}
if strings.Contains(item.label, "Create New Project") {
createItem = item
continue
}
newItems = append(newItems, item)
}
if len(items) <= 10 {
return items, defaultIndex
}
createAdded := 0
if createItem.label != "" {
createAdded++
returnItems = append(returnItems, createItem)
}
defaultAdded := 0
if defaultItem.value != "" {
defaultAdded++
returnItems = append(returnItems, defaultItem)
}
selectedIndex = (createAdded + defaultAdded) - 1
if selectedIndex < 0 {
selectedIndex = 0
}
returnItems = append(returnItems, newItems...)
return returnItems, selectedIndex
}
func (p picker) Init() tea.Cmd {
return tea.Batch(p.spinner.Tick, p.preProcessor)
}
func (p picker) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case []list.Item:
p.state = "displaying"
items := []list.Item(msg)
offset := len(p.list.Items())
for i, v := range items {
p.list.InsertItem(i+offset, v)
}
tmp, selectedIndex := positionDefault(p.list.Items(), p.defaultValue)
p.list.SetItems(tmp)
p.list.Select(selectedIndex)
return p, p.spinner.Tick
case errMsg:
p.state = "idle"
p.err = msg
p.target = msg.target
return p, nil
case successMsg:
p.state = "idle"
newValue := p.value
if msg.msg == "prependProject" {
currentProject := p.queue.Get("currentProject").(string)
newValue = fmt.Sprintf("%s-%s", currentProject, newValue)
}
if !msg.unset && !p.omitFromSettings {
p.queue.stack.AddSetting(p.key, newValue)
}
return p.queue.next()
case tea.KeyMsg:
if p.list.FilterState() == list.Filtering {
break
}
switch keypress := msg.String(); keypress {
case "alt+b", "ctrl+b":
return p.queue.prev()
case "ctrl+c":
return p.queue.exitPage()
case "enter":
if p.state == "displaying" {
i, ok := p.list.SelectedItem().(item)
if ok {
p.value = string(i.value)
}
if !p.omitFromSettings {
p.queue.stack.AddSetting(p.key, p.value)
}
if p.postProcessor != nil {
if p.state != "querying" {
p.state = "querying"
p.err = nil
var cmd tea.Cmd
var cmdSpin tea.Cmd
cmd = p.postProcessor(p.value, p.queue)
p.spinner, cmdSpin = p.spinner.Update(msg)
return p, tea.Batch(cmd, cmdSpin)
}
return p, nil
}
return p.queue.next()
}
if p.err != nil && p.target != "" {
p.queue.clear(p.target)
return p.queue.goToModel(p.target)
}
}
default:
var cmdList tea.Cmd
var cmdSpin tea.Cmd
p.list, cmdList = p.list.Update(msg)
p.spinner, cmdSpin = p.spinner.Update(msg)
return p, tea.Batch(cmdSpin, cmdList)
}
// If this isn't here, then keyPress events do not get responded to by
// the list ¯\(°_o)/¯
if p.state == "displaying" {
var cmd tea.Cmd
p.list, cmd = p.list.Update(msg)
return p, cmd
}
return p, nil
}
func (p picker) View() string {
if p.preViewFunc != nil {
p.preViewFunc(p.queue)
}
doc := strings.Builder{}
doc.WriteString(p.queue.header.render())
if p.showProgress && p.err == nil {
doc.WriteString(drawProgress(p.queue.calcPercent()))
doc.WriteString("\n\n")
}
if p.err != nil {
doc.WriteString(errorAlert{p.err.(errMsg)}.Render())
return docStyle.Render(doc.String())
}
if len(p.content) > 0 {
inst := strings.Builder{}
for _, v := range p.content {
content := v.render()
inst.WriteString(content)
}
doc.WriteString(instructionStyle.Width(width).Render(inst.String()))
doc.WriteString("\n")
doc.WriteString("\n")
}
if p.state != "waiting" && p.state != "idle" && p.state != "querying" {
selectedItemStyle.Width(hardWidthLimit)
doc.WriteString(componentStyle.Render(p.list.View()))
}
if p.state == "querying" {
spinnerSB := strings.Builder{}
spinnerSB.WriteString(textStyle.Render(fmt.Sprintf("%s ", p.spinnerLabel)))
spinnerSB.WriteString(spinnerStyle.Render(fmt.Sprintf("%s", p.spinner.View())))
if p.querySlowText != "" {
spinnerSB.WriteString(textStyle.Render(fmt.Sprintf("\n%s ", p.querySlowText)))
}
doc.WriteString(bodyStyle.Render(spinnerSB.String()))
}
return docStyle.Render(doc.String())
}