Tear down everything
Fix config
This commit is contained in:
556
internal/taskwarrior/models.go
Normal file
556
internal/taskwarrior/models.go
Normal file
@@ -0,0 +1,556 @@
|
||||
package taskwarrior
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
dtformat = "20060102T150405Z"
|
||||
)
|
||||
|
||||
type UdaType string
|
||||
|
||||
const (
|
||||
UdaTypeString UdaType = "string"
|
||||
UdaTypeDate UdaType = "date"
|
||||
UdaTypeNumeric UdaType = "numeric"
|
||||
UdaTypeDuration UdaType = "duration"
|
||||
)
|
||||
|
||||
type Uda struct {
|
||||
Name string
|
||||
Type UdaType
|
||||
Label string
|
||||
Values []string
|
||||
Default string
|
||||
}
|
||||
|
||||
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 Tasks []*Task
|
||||
|
||||
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,omitempty"`
|
||||
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:"parenttask,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:"-"`
|
||||
}
|
||||
|
||||
// TODO: fix pointer receiver
|
||||
func NewTask() Task {
|
||||
return Task{
|
||||
Tags: make([]string, 0),
|
||||
Depends: make([]string, 0),
|
||||
Udas: make(map[string]any),
|
||||
}
|
||||
}
|
||||
|
||||
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 t.Udas["details"] != nil && t.Udas["details"] != "" {
|
||||
return fmt.Sprintf("%s [D]", t.Description)
|
||||
} else {
|
||||
return t.Description
|
||||
}
|
||||
|
||||
// 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 "parenttask":
|
||||
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:
|
||||
// TODO: format according to UDA type
|
||||
if val, ok := t.Udas[field]; ok {
|
||||
if strVal, ok := val.(string); ok {
|
||||
return strVal
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
slog.Error("Field not implemented", "field", field)
|
||||
return ""
|
||||
}
|
||||
|
||||
func (t *Task) GetDate(field string) time.Time {
|
||||
var dateString string
|
||||
switch field {
|
||||
case "due":
|
||||
dateString = t.Due
|
||||
case "wait":
|
||||
dateString = t.Wait
|
||||
case "scheduled":
|
||||
dateString = t.Scheduled
|
||||
case "until":
|
||||
dateString = t.Until
|
||||
case "start":
|
||||
dateString = t.Start
|
||||
case "end":
|
||||
dateString = t.End
|
||||
case "entry":
|
||||
dateString = t.Entry
|
||||
case "modified":
|
||||
dateString = t.Modified
|
||||
default:
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
if dateString == "" {
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
dt, err := time.Parse(dtformat, dateString)
|
||||
if err != nil {
|
||||
slog.Error("Failed to parse time", "error", 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Task) UnmarshalJSON(data []byte) error {
|
||||
type Alias Task
|
||||
task := Alias{}
|
||||
|
||||
if err := json.Unmarshal(data, &task); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*t = Task(task)
|
||||
|
||||
m := make(map[string]any)
|
||||
|
||||
if err := json.Unmarshal(data, &m); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
delete(m, "id")
|
||||
delete(m, "uuid")
|
||||
delete(m, "description")
|
||||
delete(m, "project")
|
||||
// delete(m, "priority")
|
||||
delete(m, "status")
|
||||
delete(m, "tags")
|
||||
delete(m, "depends")
|
||||
delete(m, "urgency")
|
||||
delete(m, "parenttask")
|
||||
delete(m, "due")
|
||||
delete(m, "wait")
|
||||
delete(m, "scheduled")
|
||||
delete(m, "until")
|
||||
delete(m, "start")
|
||||
delete(m, "end")
|
||||
delete(m, "entry")
|
||||
delete(m, "modified")
|
||||
delete(m, "recur")
|
||||
delete(m, "annotations")
|
||||
t.Udas = m
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Task) MarshalJSON() ([]byte, error) {
|
||||
type Alias Task
|
||||
task := Alias(*t)
|
||||
|
||||
knownFields, err := json.Marshal(task)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var knownMap map[string]any
|
||||
if err := json.Unmarshal(knownFields, &knownMap); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for key, value := range t.Udas {
|
||||
if value != nil && value != "" {
|
||||
knownMap[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
return json.Marshal(knownMap)
|
||||
}
|
||||
|
||||
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", "error", 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("Date format not implemented", "format", 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")
|
||||
}
|
||||
|
||||
func ValidateNumeric(s string) error {
|
||||
if _, err := strconv.ParseFloat(s, 64); err != nil {
|
||||
return fmt.Errorf("invalid number")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ValidateDuration(s string) error {
|
||||
// TODO: implement duration validation
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user