package tui import ( "errors" "fmt" "strconv" "time" "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" ) type FormField struct { textinput.Model label string } func (ff FormField) Update(msg tea.Msg) (FormField, tea.Cmd) { field, cmd := ff.Model.Update(msg) ff.Model = field return ff, cmd } func newTimestampField(label string) FormField { f := FormField{ Model: textinput.New(), label: label, } f.Validate = func(s string) error { if _, err := time.Parse(time.DateTime, s); err != nil { return fmt.Errorf("timestamps must be written like \"%s\"", time.DateTime) } return nil } return f } func newOptionalTimestampField(label string) FormField { f := FormField{ Model: textinput.New(), label: label, } f.Validate = func(s string) error { if s == "" { return nil } if _, err := time.Parse(time.DateTime, s); err != nil { return fmt.Errorf("timestamps must be written like \"%s\"", time.DateTime) } return nil } return f } func newOptionalFloatField(label string) FormField { f := FormField{ Model: textinput.New(), label: label, } f.Validate = func(s string) error { if s == "" { return nil } _, err := strconv.ParseFloat(s, 64) if err != nil { return errors.New("numerical values only") } return nil } return f } type Form struct { fields []FormField selIdx int err error SelectedStyle *lipgloss.Style UnselectedStyle *lipgloss.Style } func NewForm(fields []FormField) Form { return Form{fields: fields} } func (ff Form) Error() error { for _, field := range ff.fields { if field.Err != nil { return field.Err } } return nil } func NewEntryEditorForm() Form { form := NewForm([]FormField{ newTimestampField("Start time"), newOptionalTimestampField("End time"), {Model: textinput.New(), label: "Client"}, {Model: textinput.New(), label: "Project"}, {Model: textinput.New(), label: "Description"}, newOptionalFloatField("Hourly Rate"), }) form.SelectedStyle = &modalFocusedInputStyle form.UnselectedStyle = &modalBlurredInputStyle return form } func NewClientForm() Form { form := NewForm([]FormField{ {Model: textinput.New(), label: "Name"}, {Model: textinput.New(), label: "Email"}, newOptionalFloatField("Hourly Rate"), }) form.SelectedStyle = &modalFocusedInputStyle form.UnselectedStyle = &modalBlurredInputStyle return form } func NewProjectForm() Form { form := NewForm([]FormField{ {Model: textinput.New(), label: "Name"}, {Model: textinput.New(), label: "Client"}, newOptionalFloatField("Hourly Rate"), }) form.SelectedStyle = &modalFocusedInputStyle form.UnselectedStyle = &modalBlurredInputStyle return form } func (ff Form) Update(msg tea.Msg) (Form, tea.Cmd) { if msg, ok := msg.(tea.KeyMsg); ok { switch msg.String() { case "tab": ff.fields[ff.selIdx].Blur() ff.selIdx = (ff.selIdx + 1) % len(ff.fields) return ff, ff.fields[ff.selIdx].Focus() case "shift+tab": ff.fields[ff.selIdx].Blur() ff.selIdx-- if ff.selIdx < 0 { ff.selIdx += len(ff.fields) } return ff, ff.fields[ff.selIdx].Focus() } } field, cmd := ff.fields[ff.selIdx].Update(msg) ff.fields[ff.selIdx] = field return ff, cmd } func (ff Form) View() string { content := "" if ff.err != nil { content += errorStyle.Render(ff.err.Error()) + "\n\n" } for i, field := range ff.fields { if i > 0 { content += "\n\n" } content += field.label + ":\n" style := ff.UnselectedStyle if i == ff.selIdx { style = ff.SelectedStyle } if style != nil { content += style.Render(field.View()) } else { content += field.View() } if field.Err != nil { content += "\n" + errorStyle.Render(field.Err.Error()) } } return content } var ( modalFocusedInputStyle = lipgloss.NewStyle(). Border(lipgloss.DoubleBorder()). BorderForeground(lipgloss.Color("238")) modalBlurredInputStyle = lipgloss.NewStyle(). Border(lipgloss.NormalBorder()). BorderForeground(lipgloss.Color("238")) errorStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("196")) )