[WIP] Add task editing
This commit is contained in:
@ -93,7 +93,8 @@ func (p ReportPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
case BackMsg:
|
case BackMsg:
|
||||||
p.subpageActive = false
|
p.subpageActive = false
|
||||||
case TaskMsg:
|
case TaskMsg:
|
||||||
p.populateTaskTable(msg)
|
p.tasks = taskwarrior.Tasks(msg)
|
||||||
|
p.populateTaskTable(p.tasks)
|
||||||
case UpdateReportMsg:
|
case UpdateReportMsg:
|
||||||
p.activeReport = msg
|
p.activeReport = msg
|
||||||
cmds = append(cmds, p.getTasks())
|
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())
|
cmds = append(cmds, p.getTasks())
|
||||||
case AddedTaskMsg:
|
case AddedTaskMsg:
|
||||||
cmds = append(cmds, p.getTasks())
|
cmds = append(cmds, p.getTasks())
|
||||||
|
case EditedTaskMsg:
|
||||||
|
cmds = append(cmds, p.getTasks())
|
||||||
case tea.KeyMsg:
|
case tea.KeyMsg:
|
||||||
switch {
|
switch {
|
||||||
case key.Matches(msg, p.common.Keymap.Quit):
|
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.subpageActive = true
|
||||||
p.common.PageStack.Push(p)
|
p.common.PageStack.Push(p)
|
||||||
return p.subpage, nil
|
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):
|
case key.Matches(msg, p.common.Keymap.SetProject):
|
||||||
p.subpage = NewProjectPickerPage(p.common, p.activeProject)
|
p.subpage = NewProjectPickerPage(p.common, p.activeProject)
|
||||||
p.subpage.Init()
|
p.subpage.Init()
|
||||||
@ -152,7 +161,7 @@ func (p ReportPage) View() string {
|
|||||||
return p.common.Styles.Main.Render(p.taskTable.View()) + "\n"
|
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)
|
nCols := len(p.activeReport.Columns)
|
||||||
columns := make([]table.Column, 0)
|
columns := make([]table.Column, 0)
|
||||||
columnSizes := make([]int, nCols)
|
columnSizes := make([]int, nCols)
|
||||||
@ -162,7 +171,7 @@ func (p *ReportPage) populateTaskTable(tasks []*taskwarrior.Task) {
|
|||||||
for i, task := range tasks {
|
for i, task := range tasks {
|
||||||
row := table.Row{}
|
row := table.Row{}
|
||||||
for i, col := range p.activeReport.Columns {
|
for i, col := range p.activeReport.Columns {
|
||||||
field := task.Get(col)
|
field := task.GetString(col)
|
||||||
columnSizes[i] = max(columnSizes[i], len(field))
|
columnSizes[i] = max(columnSizes[i], len(field))
|
||||||
row = append(row, field)
|
row = append(row, field)
|
||||||
}
|
}
|
||||||
@ -204,8 +213,8 @@ func (p *ReportPage) getTasks() tea.Cmd {
|
|||||||
if p.activeProject != "" {
|
if p.activeProject != "" {
|
||||||
filters = append(filters, "project:"+p.activeProject)
|
filters = append(filters, "project:"+p.activeProject)
|
||||||
}
|
}
|
||||||
p.tasks = p.common.TW.GetTasks(p.activeReport, filters...)
|
tasks := p.common.TW.GetTasks(p.activeReport, filters...)
|
||||||
return TaskMsg(p.tasks)
|
return TaskMsg(tasks)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -16,6 +16,7 @@ type TaskEditorPage struct {
|
|||||||
common *common.Common
|
common *common.Common
|
||||||
task taskwarrior.Task
|
task taskwarrior.Task
|
||||||
form *huh.Form
|
form *huh.Form
|
||||||
|
edit bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type TaskEditorKeys struct {
|
type TaskEditorKeys struct {
|
||||||
@ -32,6 +33,10 @@ func NewTaskEditorPage(common *common.Common, task taskwarrior.Task) *TaskEditor
|
|||||||
task: task,
|
task: task,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if task.Uuid != "" {
|
||||||
|
p.edit = true
|
||||||
|
}
|
||||||
|
|
||||||
if p.task.Priority == "" {
|
if p.task.Priority == "" {
|
||||||
p.task.Priority = "(none)"
|
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 {
|
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()
|
model, err := p.common.PageStack.Pop()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("page stack empty")
|
slog.Error("page stack empty")
|
||||||
@ -139,8 +148,15 @@ func (p *TaskEditorPage) addTaskCmd() tea.Msg {
|
|||||||
return AddedTaskMsg{}
|
return AddedTaskMsg{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *TaskEditorPage) editTaskCmd() tea.Msg {
|
||||||
|
p.common.TW.ModifyTask(&p.task)
|
||||||
|
return EditedTaskMsg{}
|
||||||
|
}
|
||||||
|
|
||||||
type AddedTaskMsg struct{}
|
type AddedTaskMsg struct{}
|
||||||
|
|
||||||
|
type EditedTaskMsg struct{}
|
||||||
|
|
||||||
// TODO: move this to taskwarrior; add missing date formats
|
// TODO: move this to taskwarrior; add missing date formats
|
||||||
func validateDate(s string) error {
|
func validateDate(s string) error {
|
||||||
formats := []string{
|
formats := []string{
|
||||||
|
|||||||
@ -3,6 +3,7 @@ package taskwarrior
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
"math"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@ -18,19 +19,21 @@ type Task struct {
|
|||||||
Tags []string `json:"tags"`
|
Tags []string `json:"tags"`
|
||||||
Depends []string `json:"depends"`
|
Depends []string `json:"depends"`
|
||||||
Urgency float32 `json:"urgency"`
|
Urgency float32 `json:"urgency"`
|
||||||
|
Parent string `json:"parent"`
|
||||||
Due string `json:"due"`
|
Due string `json:"due"`
|
||||||
Wait string `json:"wait"`
|
Wait string `json:"wait"`
|
||||||
Scheduled string `json:"scheduled"`
|
Scheduled string `json:"scheduled"`
|
||||||
Until string `json:"until"`
|
Until string `json:"until"`
|
||||||
Recur string `json:"recur"`
|
|
||||||
Start string `json:"start"`
|
Start string `json:"start"`
|
||||||
End string `json:"end"`
|
End string `json:"end"`
|
||||||
Entry string `json:"entry"`
|
Entry string `json:"entry"`
|
||||||
Modified string `json:"modified"`
|
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 {
|
switch field {
|
||||||
case "id":
|
case "id":
|
||||||
return strconv.FormatInt(t.Id, 10)
|
return strconv.FormatInt(t.Id, 10)
|
||||||
@ -49,30 +52,24 @@ func (t *Task) Get(field string) string {
|
|||||||
case "urgency":
|
case "urgency":
|
||||||
return fmt.Sprintf("%.2f", t.Urgency)
|
return fmt.Sprintf("%.2f", t.Urgency)
|
||||||
case "due":
|
case "due":
|
||||||
return t.Due
|
return formatDate(t.Due, format)
|
||||||
case "wait":
|
case "wait":
|
||||||
return t.Wait
|
return t.Wait
|
||||||
case "scheduled":
|
case "scheduled":
|
||||||
return t.Scheduled
|
return formatDate(t.Scheduled, format)
|
||||||
case "end":
|
case "end":
|
||||||
return t.End
|
return t.End
|
||||||
case "entry":
|
case "entry":
|
||||||
return t.Entry
|
return formatDate(t.Entry, format)
|
||||||
case "modified":
|
case "modified":
|
||||||
return t.Modified
|
return t.Modified
|
||||||
|
case "start":
|
||||||
|
return formatDate(t.Start, format)
|
||||||
|
case "until":
|
||||||
|
return formatDate(t.Until, format)
|
||||||
// TODO: implement these fields
|
// TODO: implement these fields
|
||||||
case "start.age":
|
|
||||||
return formatTime(t.Start)
|
|
||||||
case "depends":
|
case "depends":
|
||||||
return strings.Join(t.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":
|
case "recur":
|
||||||
return t.Recur
|
return t.Recur
|
||||||
default:
|
default:
|
||||||
@ -104,15 +101,75 @@ type Report struct {
|
|||||||
|
|
||||||
type Reports map[string]*Report
|
type Reports map[string]*Report
|
||||||
|
|
||||||
func formatTime(timeStr string) string {
|
func formatDate(date string, format string) string {
|
||||||
if timeStr == "" {
|
if date == "" {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
format := "20060102T150405Z"
|
|
||||||
t, err := time.Parse(format, timeStr)
|
dtformat := "20060102T150405Z"
|
||||||
|
dt, err := time.Parse(dtformat, date)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("Failed to parse time:", err)
|
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)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -87,6 +87,7 @@ type TaskWarrior interface {
|
|||||||
|
|
||||||
GetTasks(report *Report, filter ...string) Tasks
|
GetTasks(report *Report, filter ...string) Tasks
|
||||||
AddTask(task *Task) error
|
AddTask(task *Task) error
|
||||||
|
ModifyTask(task *Task)
|
||||||
}
|
}
|
||||||
|
|
||||||
type TaskSquire struct {
|
type TaskSquire struct {
|
||||||
@ -323,6 +324,24 @@ func (ts *TaskSquire) AddTask(task *Task) error {
|
|||||||
return nil
|
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 {
|
func (ts *TaskSquire) extractConfig() *TWConfig {
|
||||||
cmd := exec.Command(twBinary, append(ts.defaultArgs, []string{"_show"}...)...)
|
cmd := exec.Command(twBinary, append(ts.defaultArgs, []string{"_show"}...)...)
|
||||||
output, err := cmd.CombinedOutput()
|
output, err := cmd.CombinedOutput()
|
||||||
|
|||||||
Reference in New Issue
Block a user