Add timestamp editor
This commit is contained in:
@ -4,61 +4,65 @@ import (
|
||||
"log/slog"
|
||||
"strings"
|
||||
"tasksquire/common"
|
||||
"tasksquire/components/timestampeditor"
|
||||
"tasksquire/timewarrior"
|
||||
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
"github.com/charmbracelet/bubbles/textinput"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/huh"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
type TimeEditorPage struct {
|
||||
common *common.Common
|
||||
interval *timewarrior.Interval
|
||||
form *huh.Form
|
||||
|
||||
startStr string
|
||||
endStr string
|
||||
tagsStr string
|
||||
// Fields
|
||||
startEditor *timestampeditor.TimestampEditor
|
||||
endEditor *timestampeditor.TimestampEditor
|
||||
tagsInput textinput.Model
|
||||
adjust bool
|
||||
|
||||
// State
|
||||
currentField int
|
||||
totalFields int
|
||||
}
|
||||
|
||||
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),
|
||||
}
|
||||
// Create start timestamp editor
|
||||
startEditor := timestampeditor.New(com).
|
||||
Title("Start").
|
||||
ValueFromString(interval.Start)
|
||||
|
||||
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)
|
||||
// Create end timestamp editor
|
||||
endEditor := timestampeditor.New(com).
|
||||
Title("End").
|
||||
ValueFromString(interval.End)
|
||||
|
||||
// Create tags input
|
||||
tagsInput := textinput.New()
|
||||
tagsInput.Placeholder = "Space separated, use \"\" for tags with spaces"
|
||||
tagsInput.SetValue(formatTags(interval.Tags))
|
||||
tagsInput.Width = 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 {
|
||||
return p.form.Init()
|
||||
// Focus the first field
|
||||
p.currentField = 0
|
||||
return p.startEditor.Focus()
|
||||
}
|
||||
|
||||
func (p *TimeEditorPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
@ -66,41 +70,165 @@ func (p *TimeEditorPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
if key.Matches(msg, p.common.Keymap.Back) {
|
||||
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)
|
||||
}
|
||||
|
||||
form, cmd := p.form.Update(msg)
|
||||
if f, ok := form.(*huh.Form); ok {
|
||||
p.form = f
|
||||
// Update the currently focused field
|
||||
var cmd tea.Cmd
|
||||
switch p.currentField {
|
||||
case 0:
|
||||
var model tea.Model
|
||||
model, cmd = p.startEditor.Update(msg)
|
||||
if editor, ok := model.(*timestampeditor.TimestampEditor); ok {
|
||||
p.startEditor = editor
|
||||
}
|
||||
case 1:
|
||||
var model tea.Model
|
||||
model, cmd = p.endEditor.Update(msg)
|
||||
if editor, ok := model.(*timestampeditor.TimestampEditor); ok {
|
||||
p.endEditor = editor
|
||||
}
|
||||
case 2:
|
||||
p.tagsInput, cmd = p.tagsInput.Update(msg)
|
||||
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)
|
||||
|
||||
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) focusCurrentField() tea.Cmd {
|
||||
switch p.currentField {
|
||||
case 0:
|
||||
return p.startEditor.Focus()
|
||||
case 1:
|
||||
return p.endEditor.Focus()
|
||||
case 2:
|
||||
p.tagsInput.Focus()
|
||||
return nil
|
||||
case 3:
|
||||
// Adjust checkbox doesn't need focus action
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *TimeEditorPage) blurCurrentField() {
|
||||
switch p.currentField {
|
||||
case 0:
|
||||
p.startEditor.Blur()
|
||||
case 1:
|
||||
p.endEditor.Blur()
|
||||
case 2:
|
||||
p.tagsInput.Blur()
|
||||
case 3:
|
||||
// Adjust checkbox doesn't need blur action
|
||||
}
|
||||
}
|
||||
|
||||
func (p *TimeEditorPage) View() string {
|
||||
return p.form.View()
|
||||
var sections []string
|
||||
|
||||
// Title
|
||||
titleStyle := p.common.Styles.Form.Focused.Title
|
||||
sections = append(sections, titleStyle.Render("Edit Time Interval"))
|
||||
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, "")
|
||||
|
||||
// Tags input
|
||||
tagsLabelStyle := p.common.Styles.Form.Focused.Title
|
||||
tagsLabel := tagsLabelStyle.Render("Tags")
|
||||
if p.currentField == 2 {
|
||||
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.Value()))
|
||||
}
|
||||
|
||||
sections = append(sections, "")
|
||||
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) {
|
||||
@ -117,13 +245,13 @@ func (p *TimeEditorPage) saveInterval() {
|
||||
}
|
||||
}
|
||||
|
||||
p.interval.Start = p.startStr
|
||||
p.interval.End = p.endStr
|
||||
p.interval.Start = p.startEditor.GetValueString()
|
||||
p.interval.End = p.endEditor.GetValueString()
|
||||
|
||||
// Parse tags
|
||||
p.interval.Tags = parseTags(p.tagsStr)
|
||||
p.interval.Tags = parseTags(p.tagsInput.Value())
|
||||
|
||||
err := p.common.TimeW.ModifyInterval(p.interval)
|
||||
err := p.common.TimeW.ModifyInterval(p.interval, p.adjust)
|
||||
if err != nil {
|
||||
slog.Error("Failed to modify interval", "err", err)
|
||||
}
|
||||
@ -164,5 +292,3 @@ func formatTags(tags []string) string {
|
||||
}
|
||||
return strings.Join(formatted, " ")
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user