package pages import ( "log/slog" "strings" "tasksquire/common" "tasksquire/timewarrior" "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/huh" ) type TimeEditorPage struct { common *common.Common interval *timewarrior.Interval form *huh.Form startStr string endStr string tagsStr string } func NewTimeEditorPage(com *common.Common, interval *timewarrior.Interval) *TimeEditorPage { p := &TimeEditorPage{ common: com, interval: interval, startStr: interval.Start, endStr: interval.End, tagsStr: formatTags(interval.Tags), } p.form = huh.NewForm( huh.NewGroup( huh.NewInput(). Title("Start"). Value(&p.startStr). Validate(func(s string) error { return timewarrior.ValidateDate(s) }), huh.NewInput(). Title("End"). Value(&p.endStr). Validate(func(s string) error { if s == "" { return nil // End can be empty (active) } return timewarrior.ValidateDate(s) }), huh.NewInput(). Title("Tags"). Value(&p.tagsStr). Description("Space separated, use \"\" for tags with spaces"), ), ).WithTheme(com.Styles.Form) return p } func (p *TimeEditorPage) Init() tea.Cmd { return p.form.Init() } func (p *TimeEditorPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmds []tea.Cmd switch msg := msg.(type) { case tea.KeyMsg: if key.Matches(msg, p.common.Keymap.Back) { model, err := p.common.PopPage() if err != nil { slog.Error("page stack empty") return nil, tea.Quit } return model, BackCmd } case tea.WindowSizeMsg: p.SetSize(msg.Width, msg.Height) } form, cmd := p.form.Update(msg) if f, ok := form.(*huh.Form); ok { p.form = f } cmds = append(cmds, cmd) if p.form.State == huh.StateCompleted { p.saveInterval() model, err := p.common.PopPage() if err != nil { slog.Error("page stack empty") return nil, tea.Quit } // Return with a command to refresh the intervals return model, tea.Batch(tea.Batch(cmds...), refreshIntervals) } return p, tea.Batch(cmds...) } func (p *TimeEditorPage) View() string { return p.form.View() } func (p *TimeEditorPage) SetSize(width, height int) { p.common.SetSize(width, height) } func (p *TimeEditorPage) saveInterval() { // If it's an existing interval (has ID), delete it first so we can replace it with the modified version if p.interval.ID != 0 { err := p.common.TimeW.DeleteInterval(p.interval.ID) if err != nil { slog.Error("Failed to delete old interval during edit", "err", err) // Proceeding to import anyway, attempting to save user data } } p.interval.Start = p.startStr p.interval.End = p.endStr // Parse tags p.interval.Tags = parseTags(p.tagsStr) err := p.common.TimeW.ModifyInterval(p.interval) if err != nil { slog.Error("Failed to modify interval", "err", err) } } func parseTags(tagsStr string) []string { var tags []string var current strings.Builder inQuotes := false for _, r := range tagsStr { switch { case r == '"': inQuotes = !inQuotes case r == ' ' && !inQuotes: if current.Len() > 0 { tags = append(tags, current.String()) current.Reset() } default: current.WriteRune(r) } } if current.Len() > 0 { tags = append(tags, current.String()) } return tags } func formatTags(tags []string) string { var formatted []string for _, t := range tags { if strings.Contains(t, " ") { formatted = append(formatted, "\""+t+"\"") } else { formatted = append(formatted, t) } } return strings.Join(formatted, " ") }