tui/text_input.go (145 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" "strings" "github.com/charmbracelet/bubbles/spinner" "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" ) type textInput struct { dynamicPage label string ti textinput.Model } func newTextInput(label, defaultValue, key, spinnerLabel string) textInput { t := textInput{} t.key = key t.label = label t.state = "idle" t.spinnerLabel = spinnerLabel ti := textinput.New() ti.Placeholder = defaultValue ti.Focus() ti.CharLimit = 156 ti.Width = hardWidthLimit t.ti = ti s := spinner.New() s.Spinner = spinnerType t.spinner = s t.showProgress = true return t } func (p textInput) Init() tea.Cmd { return tea.Batch(textinput.Blink, p.spinner.Tick) } func (p textInput) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmd tea.Cmd keyTarget := strings.ReplaceAll(p.key, projNewSuffix, "") // if the intended key for this setting is already set, skip if p.queue.stack.GetSetting(p.key) != "" || p.queue.stack.GetSetting(keyTarget) != "" { return p.queue.next() } switch msg := msg.(type) { case tea.KeyMsg: switch keypress := msg.String(); keypress { case "ctrl+c": return p.queue.exitPage() case "alt+b", "ctrl+b": return p.queue.prev() case "enter": val := p.ti.Value() if val == "" { val = p.ti.Placeholder } // TODO: see if you can figure out a test for these empty bits if val == "" { p.err = fmt.Errorf("You must enter a value") return p, nil } p.value = val // TODO: see if you can figure out a test for these untested bits if p.postProcessor != nil { if p.state != "querying" { p.state = "querying" p.err = nil return p, p.postProcessor(p.value, p.queue) } return p, nil } if !p.omitFromSettings { p.queue.stack.AddSetting(p.key, p.value) } return p.queue.next() } // We handle errors just like any other message case errMsg: p.err = msg p.state = "idle" if msg.quit { return p, tea.Quit } var cmdSpin tea.Cmd p.spinner, cmdSpin = p.spinner.Update(msg) return p, cmdSpin case successMsg: // Filter project creation screens screeens newKey := strings.ReplaceAll(p.key, projNewSuffix, "") newValue := p.value if msg.msg == "prependProject" { currentProject := p.queue.Get("currentProject").(string) newValue = fmt.Sprintf("%s-%s", currentProject, newValue) } if !p.omitFromSettings { p.queue.stack.AddSetting(newKey, newValue) } return p.queue.next() } var cmdSpin tea.Cmd p.spinner, cmdSpin = p.spinner.Update(msg) p.ti, cmd = p.ti.Update(msg) return p, tea.Batch(cmd, cmdSpin) } func (p textInput) View() string { if p.preViewFunc != nil { p.preViewFunc(p.queue) } doc := strings.Builder{} doc.WriteString(p.queue.header.render()) if p.showProgress { doc.WriteString(drawProgress(p.queue.calcPercent())) doc.WriteString("\n\n") } doc.WriteString(bodyStyle.Render(titleStyle.Render(fmt.Sprintf("%s: ", p.label)))) doc.WriteString("\n") inst := strings.Builder{} for _, v := range p.content { inst.WriteString(v.render()) } height := (len(inst.String()) / hardWidthLimit) + 1 content := instructionStyle. Width(hardWidthLimit). Height(height). Render(inst.String()) doc.WriteString(content) doc.WriteString("\n") doc.WriteString(inputText.Render(p.ti.View())) doc.WriteString("\n") if p.err != nil { height := len(p.err.Error()) / width doc.WriteString("\n") doc.WriteString(alertStyle.Width(width).Height(height).Render(fmt.Sprintf("Error: %s", p.err))) doc.WriteString("\n") } if p.state == "querying" && p.err == nil { spinnerSB := strings.Builder{} spinnerSB.WriteString(textStyle.Render(fmt.Sprintf("%s ", p.spinnerLabel))) spinnerSB.WriteString(spinnerStyle.Render(fmt.Sprintf("%s", p.spinner.View()))) doc.WriteString(bodyStyle.Render(spinnerSB.String())) doc.WriteString("\n") } if p.state != "querying" { if p.ti.Placeholder != "" { styledPlaceHolder := textInputDefaultStyle.Render(p.ti.Placeholder) doc.WriteString(textInputPrompt.Render(fmt.Sprintf("Type a value or hit enter for '%s'", styledPlaceHolder))) } else { doc.WriteString(textInputPrompt.Render("Type a value and hit enter to continue")) } } return docStyle.Render(doc.String()) }