package pages import ( "log/slog" "strings" "tasksquire/common" "tasksquire/components/autocomplete" "tasksquire/components/timestampeditor" "tasksquire/timewarrior" "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" ) type TimeEditorPage struct { common *common.Common interval *timewarrior.Interval // Fields startEditor *timestampeditor.TimestampEditor endEditor *timestampeditor.TimestampEditor tagsInput *autocomplete.Autocomplete adjust bool // State currentField int totalFields int } func NewTimeEditorPage(com *common.Common, interval *timewarrior.Interval) *TimeEditorPage { // Create start timestamp editor startEditor := timestampeditor.New(com). Title("Start"). ValueFromString(interval.Start) // Create end timestamp editor endEditor := timestampeditor.New(com). Title("End"). ValueFromString(interval.End) // Create tags autocomplete with combinations from past intervals tagCombinations := com.TimeW.GetTagCombinations() tagsInput := autocomplete.New(tagCombinations, 3, 10) tagsInput.SetPlaceholder("Space separated, use \"\" for tags with spaces") tagsInput.SetValue(formatTags(interval.Tags)) tagsInput.SetWidth(50) p := &TimeEditorPage{ common: com, interval: interval, startEditor: startEditor, endEditor: endEditor, tagsInput: tagsInput, adjust: true, // Enable :adjust by default currentField: 0, totalFields: 4, // Updated to include adjust field } return p } func (p *TimeEditorPage) Init() tea.Cmd { // Focus the first field (tags) p.currentField = 0 p.tagsInput.Focus() return p.tagsInput.Init() } func (p *TimeEditorPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmds []tea.Cmd switch msg := msg.(type) { case tea.KeyMsg: switch { case 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 key.Matches(msg, p.common.Keymap.Ok): // Save and exit p.saveInterval() model, err := p.common.PopPage() if err != nil { slog.Error("page stack empty") return nil, tea.Quit } return model, tea.Batch(tea.Batch(cmds...), refreshIntervals) case key.Matches(msg, p.common.Keymap.Next): // Move to next field p.blurCurrentField() p.currentField = (p.currentField + 1) % p.totalFields cmds = append(cmds, p.focusCurrentField()) case key.Matches(msg, p.common.Keymap.Prev): // Move to previous field p.blurCurrentField() p.currentField = (p.currentField - 1 + p.totalFields) % p.totalFields cmds = append(cmds, p.focusCurrentField()) } case tea.WindowSizeMsg: p.SetSize(msg.Width, msg.Height) } // Update the currently focused field var cmd tea.Cmd switch p.currentField { case 0: var model tea.Model model, cmd = p.tagsInput.Update(msg) if ac, ok := model.(*autocomplete.Autocomplete); ok { p.tagsInput = ac } case 1: var model tea.Model model, cmd = p.startEditor.Update(msg) if editor, ok := model.(*timestampeditor.TimestampEditor); ok { p.startEditor = editor } case 2: var model tea.Model model, cmd = p.endEditor.Update(msg) if editor, ok := model.(*timestampeditor.TimestampEditor); ok { p.endEditor = editor } case 3: // Handle adjust toggle with space/enter if msg, ok := msg.(tea.KeyMsg); ok { if msg.String() == " " || msg.String() == "enter" { p.adjust = !p.adjust } } } cmds = append(cmds, cmd) return p, tea.Batch(cmds...) } func (p *TimeEditorPage) focusCurrentField() tea.Cmd { switch p.currentField { case 0: p.tagsInput.Focus() return p.tagsInput.Init() case 1: return p.startEditor.Focus() case 2: return p.endEditor.Focus() case 3: // Adjust checkbox doesn't need focus action return nil } return nil } func (p *TimeEditorPage) blurCurrentField() { switch p.currentField { case 0: p.tagsInput.Blur() case 1: p.startEditor.Blur() case 2: p.endEditor.Blur() case 3: // Adjust checkbox doesn't need blur action } } func (p *TimeEditorPage) View() string { var sections []string // Title titleStyle := p.common.Styles.Form.Focused.Title sections = append(sections, titleStyle.Render("Edit Time Interval")) sections = append(sections, "") // Tags input (now first) tagsLabelStyle := p.common.Styles.Form.Focused.Title tagsLabel := tagsLabelStyle.Render("Tags") if p.currentField == 0 { sections = append(sections, tagsLabel) sections = append(sections, p.tagsInput.View()) descStyle := p.common.Styles.Form.Focused.Description sections = append(sections, descStyle.Render("Space separated, use \"\" for tags with spaces")) } else { blurredLabelStyle := p.common.Styles.Form.Blurred.Title sections = append(sections, blurredLabelStyle.Render("Tags")) sections = append(sections, lipgloss.NewStyle().Faint(true).Render(p.tagsInput.GetValue())) } sections = append(sections, "") sections = append(sections, "") // Start editor sections = append(sections, p.startEditor.View()) sections = append(sections, "") // End editor sections = append(sections, p.endEditor.View()) sections = append(sections, "") // Adjust checkbox adjustLabelStyle := p.common.Styles.Form.Focused.Title adjustLabel := adjustLabelStyle.Render("Adjust overlaps") var checkbox string if p.adjust { checkbox = "[X]" } else { checkbox = "[ ]" } if p.currentField == 3 { focusedStyle := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("12")) sections = append(sections, adjustLabel) sections = append(sections, focusedStyle.Render(checkbox+" Auto-adjust overlapping intervals")) descStyle := p.common.Styles.Form.Focused.Description sections = append(sections, descStyle.Render("Press space to toggle")) } else { blurredLabelStyle := p.common.Styles.Form.Blurred.Title sections = append(sections, blurredLabelStyle.Render("Adjust overlaps")) sections = append(sections, lipgloss.NewStyle().Faint(true).Render(checkbox+" Auto-adjust overlapping intervals")) } sections = append(sections, "") sections = append(sections, "") // Help text helpStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("8")) sections = append(sections, helpStyle.Render("tab/shift+tab: navigate • enter: save • esc: cancel")) return lipgloss.JoinVertical(lipgloss.Left, sections...) } 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.startEditor.GetValueString() p.interval.End = p.endEditor.GetValueString() // Parse tags p.interval.Tags = parseTags(p.tagsInput.GetValue()) err := p.common.TimeW.ModifyInterval(p.interval, p.adjust) 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, " ") }