diff options
| author | Paul Bütow <1224732+snonux@users.noreply.github.com> | 2025-06-21 23:38:44 +0300 |
|---|---|---|
| committer | Paul Bütow <1224732+snonux@users.noreply.github.com> | 2025-06-21 23:38:44 +0300 |
| commit | 28c46b7680ad168fd8d1aa9af2b37a64513a0fc3 (patch) | |
| tree | 3e55467d61499fd4276e956b46b367d40d63c2f1 /internal | |
| parent | 588c4b772cc782a2c190029e220290c89a08c8c9 (diff) | |
Add recurrence editing column and hotkey
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/ui/table.go | 104 | ||||
| -rw-r--r-- | internal/ui/table_test.go | 30 |
2 files changed, 92 insertions, 42 deletions
diff --git a/internal/ui/table.go b/internal/ui/table.go index ae3af66..1f06e7e 100644 --- a/internal/ui/table.go +++ b/internal/ui/table.go @@ -54,6 +54,10 @@ type Model struct { dueID int dueDate time.Time + recurEditing bool + recurID int + recurInput textinput.Model + filterEditing bool filterInput textinput.Model @@ -81,14 +85,15 @@ type Model struct { windowHeight int - idWidth int - priWidth int - ageWidth int - urgWidth int - dueWidth int - tagsWidth int - descWidth int - annWidth int + idWidth int + priWidth int + ageWidth int + urgWidth int + dueWidth int + recurWidth int + tagsWidth int + descWidth int + annWidth int total int inProgress int @@ -126,6 +131,8 @@ func New(filters []string) (Model, error) { m.descInput.Prompt = "description: " m.tagsInput = textinput.New() m.tagsInput.Prompt = "tags: " + m.recurInput = textinput.New() + m.recurInput.Prompt = "recur: " m.dueDate = time.Now() m.searchInput = textinput.New() m.searchInput.Prompt = "search: " @@ -147,11 +154,12 @@ func (m *Model) newTable(rows []atable.Row) (atable.Model, atable.Styles) { {Title: "ID", Width: m.idWidth}, {Title: "Pri", Width: m.priWidth}, {Title: "Age", Width: m.ageWidth}, - {Title: "Urg", Width: m.urgWidth}, {Title: "Due", Width: m.dueWidth}, + {Title: "Recur", Width: m.recurWidth}, {Title: "Tags", Width: m.tagsWidth}, {Title: "Annotations", Width: m.annWidth}, {Title: "Description", Width: m.descWidth}, + {Title: "Urg", Width: m.urgWidth}, } t := atable.New( atable.WithColumns(cols), @@ -370,6 +378,25 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } return m, nil } + if m.recurEditing { + switch msg.Type { + case tea.KeyEnter: + task.SetRecurrence(m.recurID, m.recurInput.Value()) + m.recurEditing = false + m.recurInput.Blur() + m.reload() + m.updateTableHeight() + return m, nil + case tea.KeyEsc: + m.recurEditing = false + m.recurInput.Blur() + m.updateTableHeight() + return m, nil + } + var cmd tea.Cmd + m.recurInput, cmd = m.recurInput.Update(msg) + return m, cmd + } if m.prioritySelecting { switch msg.Type { case tea.KeyEnter: @@ -527,10 +554,12 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if row := m.tbl.SelectedRow(); row != nil { idStr := ansi.Strip(row[0]) if id, err := strconv.Atoi(idStr); err == nil { - days := rand.Intn(31) + 7 - due := time.Now().AddDate(0, 0, days).Format("2006-01-02") - task.SetDueDate(id, due) - m.reload() + m.recurID = id + m.recurEditing = true + m.recurInput.SetValue(m.tasks[m.tbl.Cursor()].Recur) + m.recurInput.Focus() + m.updateTableHeight() + return m, nil } } case "p": @@ -646,7 +675,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } m.updateTableHeight() return m, nil - case 4: + case 3: m.dueID = id if ts, err := time.Parse("20060102T150405Z", m.tasks[m.tbl.Cursor()].Due); err == nil { m.dueDate = ts @@ -656,6 +685,13 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.dueEditing = true m.updateTableHeight() return m, nil + case 4: + m.recurID = id + m.recurEditing = true + m.recurInput.SetValue(m.tasks[m.tbl.Cursor()].Recur) + m.recurInput.Focus() + m.updateTableHeight() + return m, nil case 5: m.tagsID = id m.tagsEditing = true @@ -716,7 +752,7 @@ func (m Model) View() string { "D: mark task done", "U: undo done", "d: set due date", - "r: random due date", + "r: edit recurrence", "a: annotate task", "A: replace annotations", "p: set priority", @@ -773,6 +809,12 @@ func (m Model) View() string { m.tagsInput.View(), ) } + if m.recurEditing { + view = lipgloss.JoinVertical(lipgloss.Left, + view, + m.recurInput.View(), + ) + } if m.filterEditing { view = lipgloss.JoinVertical(lipgloss.Left, view, @@ -823,6 +865,7 @@ func (m Model) taskToRow(t task.Task) atable.Row { tags := strings.Join(t.Tags, " ") urg := fmt.Sprintf("%.1f", t.Urgency) + recur := t.Recur var anns []string for _, a := range t.Annotations { @@ -838,11 +881,12 @@ func (m Model) taskToRow(t task.Task) atable.Row { style.Render(strconv.Itoa(t.ID)), m.formatPriority(t.Priority, m.priWidth), style.Render(age), - style.Render(urg), m.formatDue(t.Due, m.dueWidth), + style.Render(recur), style.Render(tags), style.Render(annStr), style.Render(t.Description), + style.Render(urg), } } @@ -958,6 +1002,7 @@ func (m Model) taskToRowSearch(t task.Task, re *regexp.Regexp, styles atable.Sty tags := strings.Join(t.Tags, " ") urg := fmt.Sprintf("%.1f", t.Urgency) + recur := t.Recur var anns []string for _, a := range t.Annotations { @@ -978,8 +1023,7 @@ func (m Model) taskToRowSearch(t task.Task, re *regexp.Regexp, styles atable.Sty priStr := m.formatPriority(t.Priority, m.priWidth) ageStr := getStyle(2).Render(age) dueStr := m.formatDue(t.Due, m.dueWidth) - urgStr := getStyle(3).Render(urg) - + recurStr := m.highlightCell(getStyle(4), re, recur) tagStr := m.highlightCell(getStyle(5), re, tags) annRaw := strings.Join(anns, "; ") annCount := "" @@ -988,16 +1032,18 @@ func (m Model) taskToRowSearch(t task.Task, re *regexp.Regexp, styles atable.Sty } annStr := m.highlightCellMatch(getStyle(6), re, annRaw, annCount) descStr := m.highlightCell(getStyle(7), re, t.Description) + urgStr := getStyle(8).Render(urg) return atable.Row{ idStr, priStr, ageStr, - urgStr, dueStr, + recurStr, tagStr, annStr, descStr, + urgStr, } } @@ -1020,9 +1066,9 @@ func (m Model) expandedCellView() string { val = fmt.Sprintf("%dd", days) } case 3: - val = fmt.Sprintf("%.1f", t.Urgency) - case 4: val = ansi.Strip(m.formatDue(t.Due, m.dueWidth)) + case 4: + val = t.Recur case 5: val = strings.Join(t.Tags, " ") case 6: @@ -1033,6 +1079,8 @@ func (m Model) expandedCellView() string { val = strings.Join(anns, "; ") case 7: val = t.Description + case 8: + val = fmt.Sprintf("%.1f", t.Urgency) } header := "" cols := m.tbl.Columns() @@ -1079,7 +1127,7 @@ func (m *Model) updateTableHeight() { if m.cellExpanded { h-- } - if m.annotating || m.dueEditing || m.prioritySelecting || m.searching || m.descEditing || m.tagsEditing || m.filterEditing { + if m.annotating || m.dueEditing || m.prioritySelecting || m.searching || m.descEditing || m.tagsEditing || m.recurEditing || m.filterEditing { h-- } if h < 1 { @@ -1114,6 +1162,7 @@ func (m *Model) computeColumnWidths() { maxAge := 0 maxUrg := 0 maxDue := 0 + maxRecur := 1 maxTags := 0 maxAnn := 1 for _, t := range m.tasks { @@ -1135,6 +1184,9 @@ func (m *Model) computeColumnWidths() { if l := len(due); l > maxDue { maxDue = l } + if l := len(t.Recur); l > maxRecur { + maxRecur = l + } tags := strings.Join(t.Tags, " ") if l := len(tags); l > maxTags { maxTags = l @@ -1150,6 +1202,7 @@ func (m *Model) computeColumnWidths() { m.ageWidth = maxAge m.urgWidth = maxUrg m.dueWidth = maxDue + m.recurWidth = maxRecur m.tagsWidth = maxTags m.annWidth = maxAnn @@ -1157,8 +1210,8 @@ func (m *Model) computeColumnWidths() { if total == 0 { total = 80 } - base := m.idWidth + m.priWidth + m.ageWidth + m.urgWidth + m.dueWidth + m.tagsWidth + m.annWidth - base += 7 // spaces between columns + base := m.idWidth + m.priWidth + m.ageWidth + m.dueWidth + m.recurWidth + m.tagsWidth + m.annWidth + m.urgWidth + base += 8 // spaces between columns m.descWidth = total - base if m.descWidth < 1 { m.descWidth = 1 @@ -1174,11 +1227,12 @@ func (m *Model) applyColumns() { {Title: "ID", Width: m.idWidth}, {Title: "Pri", Width: m.priWidth}, {Title: "Age", Width: m.ageWidth}, - {Title: "Urg", Width: m.urgWidth}, {Title: "Due", Width: m.dueWidth}, + {Title: "Recur", Width: m.recurWidth}, {Title: "Tags", Width: m.tagsWidth}, {Title: "Annotations", Width: m.annWidth}, {Title: "Description", Width: m.descWidth}, + {Title: "Urg", Width: m.urgWidth}, } m.tbl.SetColumns(cols) } diff --git a/internal/ui/table_test.go b/internal/ui/table_test.go index 3bf6f3e..18a471b 100644 --- a/internal/ui/table_test.go +++ b/internal/ui/table_test.go @@ -289,17 +289,17 @@ func TestDueDateHotkey(t *testing.T) { } } -func TestRandomDueDateHotkey(t *testing.T) { +func TestRecurrenceHotkey(t *testing.T) { tmp := t.TempDir() taskPath := filepath.Join(tmp, "task") - dueFile := filepath.Join(tmp, "due.txt") + recFile := filepath.Join(tmp, "recur.txt") script := "#!/bin/sh\n" + "if echo \"$@\" | grep -q export; then\n" + " echo '{\"id\":1,\"uuid\":\"x\",\"description\":\"d\",\"status\":\"pending\",\"entry\":\"\",\"priority\":\"\",\"urgency\":0}'\n" + " exit 0\n" + "fi\n" + - "echo \"$@\" > " + dueFile + "\n" + "echo \"$@\" > " + recFile + "\n" if err := os.WriteFile(taskPath, []byte(script), 0o755); err != nil { t.Fatal(err) @@ -323,24 +323,20 @@ func TestRandomDueDateHotkey(t *testing.T) { mv, _ := m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'r'}}) m = mv.(Model) - - data, err := os.ReadFile(dueFile) - if err != nil { - t.Fatalf("read due: %v", err) + for _, r := range "daily" { + mv, _ = m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{r}}) + m = mv.(Model) } + mv, _ = m.Update(tea.KeyMsg{Type: tea.KeyEnter}) + m = mv.(Model) - parts := strings.Split(strings.TrimSpace(string(data)), " ") - if len(parts) != 3 { - t.Fatalf("unexpected command: %q", data) - } - dueStr := strings.TrimPrefix(parts[2], "due:") - dueTime, err := time.Parse("2006-01-02", dueStr) + data, err := os.ReadFile(recFile) if err != nil { - t.Fatalf("parse due: %v", err) + t.Fatalf("read recur: %v", err) } - days := int(time.Until(dueTime).Hours() / 24) - if days < 7 || days > 37 { - t.Fatalf("due date out of range: %d", days) + + if strings.TrimSpace(string(data)) != "1 modify recur:daily" { + t.Fatalf("recur not set: %q", data) } } |
