From a23b76c3c906b9097e943cb4b20143f455fa87d9 Mon Sep 17 00:00:00 2001 From: Martin Date: Tue, 21 May 2024 21:53:08 +0200 Subject: [PATCH] [WIP] Add task editing --- pages/report.go | 19 +++++-- pages/taskEditor.go | 18 ++++++- taskwarrior/models.go | 101 +++++++++++++++++++++++++++++-------- taskwarrior/taskwarrior.go | 19 +++++++ 4 files changed, 129 insertions(+), 28 deletions(-) diff --git a/pages/report.go b/pages/report.go index 743d275..601945f 100644 --- a/pages/report.go +++ b/pages/report.go @@ -93,7 +93,8 @@ func (p ReportPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case BackMsg: p.subpageActive = false case TaskMsg: - p.populateTaskTable(msg) + p.tasks = taskwarrior.Tasks(msg) + p.populateTaskTable(p.tasks) case UpdateReportMsg: p.activeReport = msg cmds = append(cmds, p.getTasks()) @@ -106,6 +107,8 @@ func (p ReportPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) { cmds = append(cmds, p.getTasks()) case AddedTaskMsg: cmds = append(cmds, p.getTasks()) + case EditedTaskMsg: + cmds = append(cmds, p.getTasks()) case tea.KeyMsg: switch { case key.Matches(msg, p.common.Keymap.Quit): @@ -128,6 +131,12 @@ func (p ReportPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) { p.subpageActive = true p.common.PageStack.Push(p) return p.subpage, nil + case key.Matches(msg, p.common.Keymap.Edit): + p.subpage = NewTaskEditorPage(p.common, *p.selectedTask) + p.subpage.Init() + p.subpageActive = true + p.common.PageStack.Push(p) + return p.subpage, nil case key.Matches(msg, p.common.Keymap.SetProject): p.subpage = NewProjectPickerPage(p.common, p.activeProject) p.subpage.Init() @@ -152,7 +161,7 @@ func (p ReportPage) View() string { return p.common.Styles.Main.Render(p.taskTable.View()) + "\n" } -func (p *ReportPage) populateTaskTable(tasks []*taskwarrior.Task) { +func (p *ReportPage) populateTaskTable(tasks taskwarrior.Tasks) { nCols := len(p.activeReport.Columns) columns := make([]table.Column, 0) columnSizes := make([]int, nCols) @@ -162,7 +171,7 @@ func (p *ReportPage) populateTaskTable(tasks []*taskwarrior.Task) { for i, task := range tasks { row := table.Row{} for i, col := range p.activeReport.Columns { - field := task.Get(col) + field := task.GetString(col) columnSizes[i] = max(columnSizes[i], len(field)) row = append(row, field) } @@ -204,8 +213,8 @@ func (p *ReportPage) getTasks() tea.Cmd { if p.activeProject != "" { filters = append(filters, "project:"+p.activeProject) } - p.tasks = p.common.TW.GetTasks(p.activeReport, filters...) - return TaskMsg(p.tasks) + tasks := p.common.TW.GetTasks(p.activeReport, filters...) + return TaskMsg(tasks) } } diff --git a/pages/taskEditor.go b/pages/taskEditor.go index 1253724..4c22fd7 100644 --- a/pages/taskEditor.go +++ b/pages/taskEditor.go @@ -16,6 +16,7 @@ type TaskEditorPage struct { common *common.Common task taskwarrior.Task form *huh.Form + edit bool } type TaskEditorKeys struct { @@ -32,6 +33,10 @@ func NewTaskEditorPage(common *common.Common, task taskwarrior.Task) *TaskEditor task: task, } + if task.Uuid != "" { + p.edit = true + } + if p.task.Priority == "" { p.task.Priority = "(none)" } @@ -118,7 +123,11 @@ func (p *TaskEditorPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } if p.form.State == huh.StateCompleted { - cmds = append(cmds, p.addTaskCmd) + if p.edit { + cmds = append(cmds, p.editTaskCmd) + } else { + cmds = append(cmds, p.addTaskCmd) + } model, err := p.common.PageStack.Pop() if err != nil { slog.Error("page stack empty") @@ -139,8 +148,15 @@ func (p *TaskEditorPage) addTaskCmd() tea.Msg { return AddedTaskMsg{} } +func (p *TaskEditorPage) editTaskCmd() tea.Msg { + p.common.TW.ModifyTask(&p.task) + return EditedTaskMsg{} +} + type AddedTaskMsg struct{} +type EditedTaskMsg struct{} + // TODO: move this to taskwarrior; add missing date formats func validateDate(s string) error { formats := []string{ diff --git a/taskwarrior/models.go b/taskwarrior/models.go index 4b8e48d..963949d 100644 --- a/taskwarrior/models.go +++ b/taskwarrior/models.go @@ -3,6 +3,7 @@ package taskwarrior import ( "fmt" "log/slog" + "math" "strconv" "strings" "time" @@ -18,19 +19,21 @@ type Task struct { Tags []string `json:"tags"` Depends []string `json:"depends"` Urgency float32 `json:"urgency"` + Parent string `json:"parent"` Due string `json:"due"` Wait string `json:"wait"` Scheduled string `json:"scheduled"` Until string `json:"until"` - Recur string `json:"recur"` Start string `json:"start"` End string `json:"end"` Entry string `json:"entry"` Modified string `json:"modified"` - Parent string `json:"parent"` + Recur string `json:"recur"` } -func (t *Task) Get(field string) string { +func (t *Task) GetString(fieldWFormat string) string { + field, format, _ := strings.Cut(fieldWFormat, ".") + switch field { case "id": return strconv.FormatInt(t.Id, 10) @@ -49,30 +52,24 @@ func (t *Task) Get(field string) string { case "urgency": return fmt.Sprintf("%.2f", t.Urgency) case "due": - return t.Due + return formatDate(t.Due, format) case "wait": return t.Wait case "scheduled": - return t.Scheduled + return formatDate(t.Scheduled, format) case "end": return t.End case "entry": - return t.Entry + return formatDate(t.Entry, format) case "modified": return t.Modified + case "start": + return formatDate(t.Start, format) + case "until": + return formatDate(t.Until, format) // TODO: implement these fields - case "start.age": - return formatTime(t.Start) case "depends": return strings.Join(t.Depends, ", ") - case "entry.age": - return formatTime(t.Entry) - case "scheduled.countdown": - return formatTime(t.Scheduled) - case "until.remaining": - return formatTime(t.Until) - case "due.relative": - return formatTime(t.Due) case "recur": return t.Recur default: @@ -104,15 +101,75 @@ type Report struct { type Reports map[string]*Report -func formatTime(timeStr string) string { - if timeStr == "" { +func formatDate(date string, format string) string { + if date == "" { return "" } - format := "20060102T150405Z" - t, err := time.Parse(format, timeStr) + + dtformat := "20060102T150405Z" + dt, err := time.Parse(dtformat, date) if err != nil { slog.Error("Failed to parse time:", err) - return timeStr + return "" + } + + switch format { + case "formatted", "": + return dt.Format("2006-01-02 15:04") + // TODO: proper julian date formatting + case "julian": + return dt.Format("060102.1504") + case "epoch": + return strconv.FormatInt(dt.Unix(), 10) + case "iso": + return dt.Format("2006-01-02T150405Z") + case "age": + return parseDurationVague(time.Since(dt)) + case "relative": + return parseDurationVague(time.Until(dt)) + // TODO: implement remaining + case "remaining": + return "" + case "countdown": + return parseCountdown(time.Since(dt)) + default: + slog.Error(fmt.Sprintf("Date format not implemented: %s", format)) + return "" } - return t.Format("2006-01-02 15:04") +} + +func parseDurationVague(d time.Duration) string { + dur := d.Round(time.Second).Abs() + days := dur.Hours() / 24 + + var formatted string + if dur >= time.Hour*24*365 { + formatted = fmt.Sprintf("%.1fy", days/365) + } else if dur >= time.Hour*24*90 { + formatted = strconv.Itoa(int(math.Round(days/30))) + "mo" + } else if dur >= time.Hour*24*7 { + formatted = strconv.Itoa(int(math.Round(days/7))) + "w" + } else if dur >= time.Hour*24 { + formatted = strconv.Itoa(int(days)) + "d" + } else if dur >= time.Hour { + formatted = strconv.Itoa(int(dur.Round(time.Hour).Hours())) + "h" + } else if dur >= time.Minute { + formatted = strconv.Itoa(int(dur.Round(time.Minute).Minutes())) + "min" + } else if dur >= time.Second { + formatted = strconv.Itoa(int(dur.Round(time.Second).Seconds())) + "s" + } + + if d < 0 { + formatted = "-" + formatted + } + + return formatted +} + +func parseCountdown(d time.Duration) string { + hours := int(d.Hours()) + minutes := int(d.Minutes()) % 60 + seconds := int(d.Seconds()) % 60 + + return fmt.Sprintf("%d:%02d:%02d", hours, minutes, seconds) } diff --git a/taskwarrior/taskwarrior.go b/taskwarrior/taskwarrior.go index 4006b10..2686443 100644 --- a/taskwarrior/taskwarrior.go +++ b/taskwarrior/taskwarrior.go @@ -87,6 +87,7 @@ type TaskWarrior interface { GetTasks(report *Report, filter ...string) Tasks AddTask(task *Task) error + ModifyTask(task *Task) } type TaskSquire struct { @@ -323,6 +324,24 @@ func (ts *TaskSquire) AddTask(task *Task) error { return nil } +// TODO error handling +func (ts *TaskSquire) ModifyTask(task *Task) { + ts.mutex.Lock() + defer ts.mutex.Unlock() + + jsonStr, err := json.Marshal(Tasks{task}) + if err != nil { + slog.Error("Failed marshalling task:", err) + } + + cmd := exec.Command(twBinary, append([]string{"echo", string(jsonStr), "|"}, append(ts.defaultArgs, []string{"import", "-"}...)...)...) + out, err := cmd.CombinedOutput() + strOut := string(out) + if err != nil { + slog.Error("Failed modifying task:", err, strOut) + } +} + func (ts *TaskSquire) extractConfig() *TWConfig { cmd := exec.Command(twBinary, append(ts.defaultArgs, []string{"_show"}...)...) output, err := cmd.CombinedOutput()