diff options
Diffstat (limited to 'internal/tui/form.go')
-rw-r--r-- | internal/tui/form.go | 164 |
1 files changed, 164 insertions, 0 deletions
diff --git a/internal/tui/form.go b/internal/tui/form.go new file mode 100644 index 0000000..4b12762 --- /dev/null +++ b/internal/tui/form.go @@ -0,0 +1,164 @@ +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 + + 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 (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 := "" + 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")) +) |