package task import ( "errors" "fmt" "strings" "git.sr.ht/~ewintr/gte/pkg/mstore" "github.com/google/uuid" ) var ( ErrOutdatedTask = errors.New("task is outdated") ) const ( FOLDER_INBOX = "INBOX" FOLDER_NEW = "New" QUOTE_PREFIX = ">" PREVIOUS_SEPARATOR = "Previous version:" FIELD_SEPARATOR = ":" FIELD_ID = "id" FIELD_ACTION = "action" ) // Task reperesents a task based on the data stored in a message type Task struct { // Id is a UUID that gets carried over when a new message is constructed Id string // Folder is the same name as the mstore folder Folder string Action string Due Date Message *mstore.Message // Current indicates whether the task represents an existing message in the mstore Current bool // Dirty indicates whether the task contains updates not present in the message Dirty bool } // New constructs a Task based on an mstore.Message. // // The data in the message is stored as key: value pairs, one per line. The line can start with quoting marks. // The subject line also contains values in the format "date - project - action". // Keys that exist more than once are merged. The one that appears first in the body takes precedence. A value present in the Body takes precedence over one in the subject. // This enables updating a task by forwarding a topposted message whith new values for fields that the user wants to update. func New(msg *mstore.Message) *Task { dirty := false id, d := FieldFromBody(FIELD_ID, msg.Body) if id == "" { id = uuid.New().String() dirty = true } if d { dirty = true } action, d := FieldFromBody(FIELD_ACTION, msg.Body) if action == "" { action = FieldFromSubject(FIELD_ACTION, msg.Subject) if action != "" { dirty = true } } if d { dirty = true } folder := msg.Folder if folder == FOLDER_INBOX { folder = FOLDER_NEW dirty = true } return &Task{ Id: id, Action: action, Folder: folder, Message: msg, Current: true, Dirty: dirty, } } func (t *Task) FormatSubject() string { return t.Action } func (t *Task) FormatBody() string { body := fmt.Sprintf("\n") order := []string{FIELD_ID, FIELD_ACTION} fields := map[string]string{ FIELD_ID: t.Id, FIELD_ACTION: t.Action, } keyLen := 0 for _, f := range order { if len(f) > keyLen { keyLen = len(f) } } for _, f := range order { key := f + FIELD_SEPARATOR for i := len(key); i <= keyLen; i++ { key += " " } line := strings.TrimSpace(fmt.Sprintf("%s %s", key, fields[f])) body += fmt.Sprintf("%s\n", line) } if t.Message != nil { body += fmt.Sprintf("\nPrevious version:\n\n%s\n", t.Message.Body) } return body } func FieldFromBody(field, body string) (string, bool) { value := "" dirty := false lines := strings.Split(body, "\n") for _, line := range lines { line = strings.TrimSpace(strings.TrimPrefix(line, QUOTE_PREFIX)) if line == PREVIOUS_SEPARATOR { return value, dirty } parts := strings.SplitN(line, FIELD_SEPARATOR, 2) if len(parts) < 2 { continue } fieldName := strings.ToLower(strings.TrimSpace(parts[0])) if fieldName == field { if value == "" { value = strings.TrimSpace(parts[1]) } else { dirty = true } } } return value, dirty } func FieldFromSubject(field, subject string) string { if field == FIELD_ACTION { return strings.ToLower(subject) } return "" }