package main import ( "fmt" "log" "strconv" "strings" "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" ) func main() { p := tea.NewProgram(initialModel()) if err := p.Start(); err != nil { log.Fatal(err) } } type ( tickMsg struct{} errMsg error ) const ( ccn = iota exp cvv ) const ( hotPink = lipgloss.Color("#FF06B7") darkGray = lipgloss.Color("#767676") ) var ( inputStyle = lipgloss.NewStyle().Foreground(hotPink) continueStyle = lipgloss.NewStyle().Foreground(darkGray) ) type model struct { inputs []textinput.Model focused int err error } // Validator functions to ensure valid input func ccnValidator(s string) error { // Credit Card Number should a string less than 20 digits // It should include 16 integers and 3 spaces if len(s) > 16+3 { return fmt.Errorf("CCN is too long") } // The last digit should be a number unless it is a multiple of 4 in which // case it should be a space if len(s)%5 == 0 && s[len(s)-1] != ' ' { return fmt.Errorf("CCN must separate groups with spaces") } if len(s)%5 != 0 && (s[len(s)-1] < '0' || s[len(s)-1] > '9') { return fmt.Errorf("CCN is invalid") } // The remaining digits should be integers c := strings.ReplaceAll(s, " ", "") _, err := strconv.ParseInt(c, 10, 64) return err } func expValidator(s string) error { // The 3 character should be a slash (/) // The rest thould be numbers e := strings.ReplaceAll(s, "/", "") _, err := strconv.ParseInt(e, 10, 64) if err != nil { return fmt.Errorf("EXP is invalid") } // There should be only one slash and it should be in the 2nd index (3rd character) if len(s) >= 3 && (strings.Index(s, "/") != 2 || strings.LastIndex(s, "/") != 2) { return fmt.Errorf("EXP is invalid") } return nil } func cvvValidator(s string) error { // The CVV should be a number of 3 digits // Since the input will already ensure that the CVV is a string of length 3, // All we need to do is check that it is a number _, err := strconv.ParseInt(s, 10, 64) return err } func initialModel() model { var inputs []textinput.Model = make([]textinput.Model, 3) inputs[ccn] = textinput.New() inputs[ccn].Placeholder = "4505 **** **** 1234" inputs[ccn].Focus() inputs[ccn].CharLimit = 20 inputs[ccn].Width = 30 inputs[ccn].Prompt = "" inputs[ccn].Validate = ccnValidator inputs[exp] = textinput.New() inputs[exp].Placeholder = "MM/YY " inputs[exp].CharLimit = 5 inputs[exp].Width = 5 inputs[exp].Prompt = "" inputs[exp].Validate = expValidator inputs[cvv] = textinput.New() inputs[cvv].Placeholder = "XXX" inputs[cvv].CharLimit = 3 inputs[cvv].Width = 5 inputs[cvv].Prompt = "" inputs[cvv].Validate = cvvValidator return model{ inputs: inputs, focused: 0, err: nil, } } func (m model) Init() tea.Cmd { return textinput.Blink } func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmds []tea.Cmd = make([]tea.Cmd, len(m.inputs)) switch msg := msg.(type) { case tea.KeyMsg: switch msg.Type { case tea.KeyEnter: if m.focused == len(m.inputs)-1 { return m, tea.Quit } else { m.nextInput() } case tea.KeyCtrlC, tea.KeyEsc: return m, tea.Quit case tea.KeyShiftTab, tea.KeyCtrlP: m.prevInput() case tea.KeyTab, tea.KeyCtrlN: m.nextInput() } for i := range m.inputs { m.inputs[i].Blur() } m.inputs[m.focused].Focus() // We handle errors just like any other message case errMsg: m.err = msg return m, nil } for i := range m.inputs { m.inputs[i], cmds[i] = m.inputs[i].Update(msg) } return m, tea.Batch(cmds...) } func (m model) View() string { return fmt.Sprintf( ` Total: $21.50: %s %s %s %s %s %s %s `, inputStyle.Width(30).Render("Card Number"), m.inputs[ccn].View(), inputStyle.Width(6).Render("EXP"), inputStyle.Width(6).Render("CVV"), m.inputs[exp].View(), m.inputs[cvv].View(), continueStyle.Render("Continue ->"), ) + "\n" } // nextInput focuses the next input field func (m *model) nextInput() { m.focused = (m.focused + 1) % len(m.inputs) } // prevInput focuses the previous input field func (m *model) prevInput() { m.focused-- // Wrap around if m.focused < 0 { m.focused = len(m.inputs) - 1 } }