summaryrefslogtreecommitdiff
path: root/internal/tui/form.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/tui/form.go')
-rw-r--r--internal/tui/form.go164
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"))
+)