415 lines
8.1 KiB
Go
415 lines
8.1 KiB
Go
package taskwarrior
|
|
|
|
import (
|
|
"fmt"
|
|
"log/slog"
|
|
"math"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
dtformat = "20060102T150405Z"
|
|
)
|
|
|
|
type Annotation struct {
|
|
Entry string `json:"entry,omitempty"`
|
|
Description string `json:"description,omitempty"`
|
|
}
|
|
|
|
func (a Annotation) String() string {
|
|
return fmt.Sprintf("%s %s", a.Entry, a.Description)
|
|
}
|
|
|
|
type Task struct {
|
|
Id int64 `json:"id,omitempty"`
|
|
Uuid string `json:"uuid,omitempty"`
|
|
Description string `json:"description,omitempty"`
|
|
Project string `json:"project"`
|
|
Priority string `json:"priority"`
|
|
Status string `json:"status,omitempty"`
|
|
Tags []string `json:"tags"`
|
|
VirtualTags []string `json:"-"`
|
|
Depends []string `json:"depends,omitempty"`
|
|
DependsIds string `json:"-"`
|
|
Urgency float32 `json:"urgency,omitempty"`
|
|
Parent string `json:"parent,omitempty"`
|
|
Due string `json:"due,omitempty"`
|
|
Wait string `json:"wait,omitempty"`
|
|
Scheduled string `json:"scheduled,omitempty"`
|
|
Until string `json:"until,omitempty"`
|
|
Start string `json:"start,omitempty"`
|
|
End string `json:"end,omitempty"`
|
|
Entry string `json:"entry,omitempty"`
|
|
Modified string `json:"modified,omitempty"`
|
|
Recur string `json:"recur,omitempty"`
|
|
Annotations []Annotation `json:"annotations,omitempty"`
|
|
Udas map[string]any `json:"-"`
|
|
}
|
|
|
|
func (t *Task) GetString(fieldWFormat string) string {
|
|
field, format, _ := strings.Cut(fieldWFormat, ".")
|
|
|
|
switch field {
|
|
case "id":
|
|
return strconv.FormatInt(t.Id, 10)
|
|
|
|
case "uuid":
|
|
if format == "short" {
|
|
return t.Uuid[:8]
|
|
}
|
|
return t.Uuid
|
|
|
|
case "description":
|
|
switch format {
|
|
case "desc":
|
|
return t.Description
|
|
case "oneline":
|
|
if len(t.Annotations) == 0 {
|
|
return t.Description
|
|
} else {
|
|
var annotations []string
|
|
for _, a := range t.Annotations {
|
|
annotations = append(annotations, a.String())
|
|
}
|
|
return fmt.Sprintf("%s %s", t.Description, strings.Join(annotations, " "))
|
|
}
|
|
case "truncated":
|
|
if len(t.Description) > 20 {
|
|
return t.Description[:20] + "..."
|
|
} else {
|
|
return t.Description
|
|
}
|
|
case "count":
|
|
return fmt.Sprintf("%s [%d]", t.Description, len(t.Annotations))
|
|
case "truncated_count":
|
|
if len(t.Description) > 20 {
|
|
return fmt.Sprintf("%s... [%d]", t.Description[:20], len(t.Annotations))
|
|
} else {
|
|
return fmt.Sprintf("%s [%d]", t.Description, len(t.Annotations))
|
|
}
|
|
}
|
|
|
|
if len(t.Annotations) == 0 {
|
|
return t.Description
|
|
} else {
|
|
// var annotations []string
|
|
// for _, a := range t.Annotations {
|
|
// annotations = append(annotations, a.String())
|
|
// }
|
|
// return fmt.Sprintf("%s\n%s", t.Description, strings.Join(annotations, "\n"))
|
|
|
|
// TODO enable support for multiline in table
|
|
return fmt.Sprintf("%s [%d]", t.Description, len(t.Annotations))
|
|
}
|
|
|
|
case "project":
|
|
switch format {
|
|
case "parent":
|
|
parent, _, _ := strings.Cut(t.Project, ".")
|
|
return parent
|
|
case "indented":
|
|
return fmt.Sprintf(" %s", t.Project)
|
|
}
|
|
return t.Project
|
|
|
|
case "priority":
|
|
return t.Priority
|
|
|
|
case "status":
|
|
return t.Status
|
|
|
|
case "tags":
|
|
switch format {
|
|
case "count":
|
|
return strconv.Itoa(len(t.Tags))
|
|
case "indicator":
|
|
if len(t.Tags) > 0 {
|
|
return "+"
|
|
} else {
|
|
return ""
|
|
}
|
|
}
|
|
return strings.Join(t.Tags, " ")
|
|
|
|
case "parent":
|
|
if format == "short" {
|
|
return t.Parent[:8]
|
|
}
|
|
return t.Parent
|
|
|
|
case "urgency":
|
|
if format == "integer" {
|
|
return strconv.Itoa(int(t.Urgency))
|
|
}
|
|
return fmt.Sprintf("%.2f", t.Urgency)
|
|
|
|
case "due":
|
|
return formatDate(t.Due, format)
|
|
|
|
case "wait":
|
|
return formatDate(t.Wait, format)
|
|
|
|
case "scheduled":
|
|
return formatDate(t.Scheduled, format)
|
|
|
|
case "end":
|
|
return formatDate(t.End, format)
|
|
|
|
case "entry":
|
|
return formatDate(t.Entry, format)
|
|
|
|
case "modified":
|
|
return formatDate(t.Modified, format)
|
|
|
|
case "start":
|
|
return formatDate(t.Start, format)
|
|
|
|
case "until":
|
|
return formatDate(t.Until, format)
|
|
|
|
case "depends":
|
|
switch format {
|
|
case "count":
|
|
return strconv.Itoa(len(t.Depends))
|
|
case "indicator":
|
|
if len(t.Depends) > 0 {
|
|
return "D"
|
|
} else {
|
|
return ""
|
|
}
|
|
}
|
|
return t.DependsIds
|
|
|
|
case "recur":
|
|
return t.Recur
|
|
|
|
default:
|
|
slog.Error(fmt.Sprintf("Field not implemented: %s", field))
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func (t *Task) GetDate(dateString string) time.Time {
|
|
dt, err := time.Parse(dtformat, dateString)
|
|
if err != nil {
|
|
slog.Error("Failed to parse time:", err)
|
|
return time.Time{}
|
|
}
|
|
return dt
|
|
}
|
|
|
|
func (t *Task) HasTag(tag string) bool {
|
|
for _, t := range t.Tags {
|
|
if t == tag {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (t *Task) AddTag(tag string) {
|
|
if !t.HasTag(tag) {
|
|
t.Tags = append(t.Tags, tag)
|
|
}
|
|
}
|
|
|
|
func (t *Task) RemoveTag(tag string) {
|
|
for i, ttag := range t.Tags {
|
|
if ttag == tag {
|
|
t.Tags = append(t.Tags[:i], t.Tags[i+1:]...)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
type Tasks []*Task
|
|
|
|
type Context struct {
|
|
Name string
|
|
Active bool
|
|
ReadFilter string
|
|
WriteFilter string
|
|
}
|
|
|
|
type Contexts map[string]*Context
|
|
|
|
type Report struct {
|
|
Name string
|
|
Description string
|
|
Labels []string
|
|
Filter string
|
|
Sort string
|
|
Columns []string
|
|
Context bool
|
|
}
|
|
|
|
type Reports map[string]*Report
|
|
|
|
func formatDate(date string, format string) string {
|
|
if date == "" {
|
|
return ""
|
|
}
|
|
|
|
dt, err := time.Parse(dtformat, date)
|
|
if err != nil {
|
|
slog.Error("Failed to parse time:", err)
|
|
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 ""
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
var (
|
|
dateFormats = []string{
|
|
"2006-01-02",
|
|
"2006-01-02T15:04",
|
|
"20060102T150405Z",
|
|
}
|
|
|
|
specialDateFormats = []string{
|
|
"",
|
|
"now",
|
|
"today",
|
|
"sod",
|
|
"eod",
|
|
"yesterday",
|
|
"tomorrow",
|
|
"monday",
|
|
"tuesday",
|
|
"wednesday",
|
|
"thursday",
|
|
"friday",
|
|
"saturday",
|
|
"sunday",
|
|
"mon",
|
|
"tue",
|
|
"wed",
|
|
"thu",
|
|
"fri",
|
|
"sat",
|
|
"sun",
|
|
"soy",
|
|
"eoy",
|
|
"soq",
|
|
"eoq",
|
|
"som",
|
|
"eom",
|
|
"socm",
|
|
"eocm",
|
|
"sow",
|
|
"eow",
|
|
"socw",
|
|
"eocw",
|
|
"soww",
|
|
"eoww",
|
|
"1st",
|
|
"2nd",
|
|
"3rd",
|
|
"4th",
|
|
"5th",
|
|
"6th",
|
|
"7th",
|
|
"8th",
|
|
"9th",
|
|
"10th",
|
|
"11th",
|
|
"12th",
|
|
"13th",
|
|
"14th",
|
|
"15th",
|
|
"16th",
|
|
"17th",
|
|
"18th",
|
|
"19th",
|
|
"20th",
|
|
"21st",
|
|
"22nd",
|
|
"23rd",
|
|
"24th",
|
|
"25th",
|
|
"26th",
|
|
"27th",
|
|
"28th",
|
|
"29th",
|
|
"30th",
|
|
"31st",
|
|
}
|
|
)
|
|
|
|
func ValidateDate(s string) error {
|
|
|
|
for _, f := range dateFormats {
|
|
if _, err := time.Parse(f, s); err == nil {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
for _, f := range specialDateFormats {
|
|
if s == f {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
return fmt.Errorf("invalid date")
|
|
}
|