Files
tasksquire/taskwarrior/models.go
2024-05-29 19:47:57 +02:00

412 lines
8.0 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"`
}
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())
}
// TODO support for multiline?
return fmt.Sprintf("%s %s", t.Description, strings.Join(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")
}