Duration is derived from the range so timer sessions can be corrected after the fact. Co-authored-by: Cursor <cursoragent@cursor.com>
155 lines
4.2 KiB
Go
155 lines
4.2 KiB
Go
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()
|
|
}
|