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: if val, ok := t.Udas[field]; ok { if strVal, ok := val.(string); ok { return strVal } } } 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") }