package common import ( "fmt" "log/slog" "math" "strconv" "strings" "time" ) 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"` 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 "" } } 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 "" } dtformat := "20060102T150405Z" 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) }