package views import ( "fmt" "strings" "time" "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "task/internal/ticket" ) // Time edit field indices. const ( FieldTimeStart = iota FieldTimeEnd TimeEditFieldCount ) const timeEditLayout = "2006-01-02 15:04" // TimeEditStyles styles the time-entry edit form. type TimeEditStyles struct { Label lipgloss.Style Focused lipgloss.Style Blurred lipgloss.Style Hint lipgloss.Style Title lipgloss.Style } // DefaultTimeEditStyles returns sensible defaults. func DefaultTimeEditStyles() TimeEditStyles { return TimeEditStyles{ Label: lipgloss.NewStyle().Foreground(lipgloss.Color("245")), Focused: lipgloss.NewStyle().Foreground(lipgloss.Color("205")), Blurred: lipgloss.NewStyle().Foreground(lipgloss.Color("240")), Hint: lipgloss.NewStyle().Foreground(lipgloss.Color("241")), Title: lipgloss.NewStyle().Bold(true), } } // TimeEditForm edits start and end of a time entry. type TimeEditForm struct { Inputs []textinput.Model Focus int Styles TimeEditStyles Width int } // NewTimeEditForm builds a form prefilled from an entry. func NewTimeEditForm(e ticket.TimeEntry, width int) TimeEditForm { styles := DefaultTimeEditStyles() inputs := make([]textinput.Model, TimeEditFieldCount) mk := func(placeholder string) textinput.Model { ti := textinput.New() ti.Placeholder = placeholder ti.CharLimit = 16 ti.Width = max(20, width-10) return ti } inputs[FieldTimeStart] = mk(timeEditLayout) inputs[FieldTimeEnd] = mk(timeEditLayout) if !e.Start.IsZero() { inputs[FieldTimeStart].SetValue(e.Start.Local().Format(timeEditLayout)) } if !e.End.IsZero() { inputs[FieldTimeEnd].SetValue(e.End.Local().Format(timeEditLayout)) } else if !e.Start.IsZero() && e.Duration > 0 { inputs[FieldTimeEnd].SetValue(e.Start.Add(e.Duration).Local().Format(timeEditLayout)) } f := TimeEditForm{ Inputs: inputs, Focus: FieldTimeStart, Styles: styles, Width: width, } f.focusIndex(FieldTimeStart) return f } func (f *TimeEditForm) focusIndex(i int) { f.Focus = i for idx := range f.Inputs { if idx == i { f.Inputs[idx].Focus() f.Inputs[idx].PromptStyle = f.Styles.Focused f.Inputs[idx].TextStyle = f.Styles.Focused } else { f.Inputs[idx].Blur() f.Inputs[idx].PromptStyle = f.Styles.Blurred f.Inputs[idx].TextStyle = f.Styles.Blurred } } } // NextField moves focus to the next field. func (f *TimeEditForm) NextField() { f.focusIndex((f.Focus + 1) % TimeEditFieldCount) } // PrevField moves focus to the previous field. func (f *TimeEditForm) PrevField() { f.focusIndex((f.Focus - 1 + TimeEditFieldCount) % TimeEditFieldCount) } // Update forwards a message to the focused text input. func (f *TimeEditForm) Update(msg tea.Msg) tea.Cmd { var cmd tea.Cmd f.Inputs[f.Focus], cmd = f.Inputs[f.Focus].Update(msg) return cmd } // Parse returns start and end in UTC, or an error. func (f TimeEditForm) Parse() (start, end time.Time, err error) { startRaw := strings.TrimSpace(f.Inputs[FieldTimeStart].Value()) endRaw := strings.TrimSpace(f.Inputs[FieldTimeEnd].Value()) if startRaw == "" || endRaw == "" { return time.Time{}, time.Time{}, fmt.Errorf("start and end are required") } start, err = time.ParseInLocation(timeEditLayout, startRaw, time.Local) if err != nil { return time.Time{}, time.Time{}, fmt.Errorf("start: use %s", timeEditLayout) } end, err = time.ParseInLocation(timeEditLayout, endRaw, time.Local) if err != nil { return time.Time{}, time.Time{}, fmt.Errorf("end: use %s", timeEditLayout) } if !end.After(start) { return time.Time{}, time.Time{}, fmt.Errorf("end must be after start") } return start.UTC(), end.UTC(), nil } // View renders the form. func (f TimeEditForm) View() string { var b strings.Builder b.WriteString(f.Styles.Title.Render("Edit time entry")) b.WriteString("\n\n") labels := []string{"Start", "End"} for i, label := range labels { b.WriteString(f.Styles.Label.Render(label+":")) b.WriteString(" ") b.WriteString(f.Inputs[i].View()) b.WriteString("\n") } b.WriteString("\n") b.WriteString(f.Styles.Hint.Render("format " + timeEditLayout + " · tab fields · enter save · esc cancel")) return b.String() }