package engine import ( "fmt" "regexp" "strings" "time" ) type Modifier struct { SetAttributes map[string]*string // key -> value (nil = clear) AttributeOrder []string // Track order of attributes for relative references AddTags []string RemoveTags []string } func NewModifier() *Modifier { return &Modifier{ SetAttributes: make(map[string]*string), AttributeOrder: []string{}, AddTags: []string{}, RemoveTags: []string{}, } } // ParseModifier parses command-line args into Modifier. // Only recognized attribute keys (ValidAttributeKeys) are accepted; // unrecognized key:value tokens produce an error. func ParseModifier(args []string) (*Modifier, error) { m := NewModifier() for _, arg := range args { if strings.HasPrefix(arg, "+") { // Add tag m.AddTags = append(m.AddTags, strings.TrimPrefix(arg, "+")) } else if strings.HasPrefix(arg, "-") && !strings.Contains(arg, ":") { // Remove tag m.RemoveTags = append(m.RemoveTags, strings.TrimPrefix(arg, "-")) } else if idx := strings.Index(arg, ":"); idx > 0 { key := arg[:idx] value := arg[idx+1:] if !ValidAttributeKeys[key] { return nil, fmt.Errorf("unknown modifier: %q (known: due, priority, project, recur, status, wait, scheduled, until)", key) } if value == "" { // Clear attribute (priority: with no value) m.SetAttributes[key] = nil } else { m.SetAttributes[key] = &value } // Track order for relative date references m.AttributeOrder = append(m.AttributeOrder, key) } } return m, nil } // Apply applies modifier to task func (m *Modifier) Apply(task *Task) error { // Track resolved dates for relative references resolvedDates := make(map[string]time.Time) // Initialize with existing task dates if task.Due != nil { resolvedDates["due"] = *task.Due } if task.Scheduled != nil { resolvedDates["scheduled"] = *task.Scheduled } if task.Wait != nil { resolvedDates["wait"] = *task.Wait } if task.Until != nil { resolvedDates["until"] = *task.Until } if task.Start != nil { resolvedDates["start"] = *task.Start } if task.End != nil { resolvedDates["end"] = *task.End } resolvedDates["created"] = task.Created resolvedDates["modified"] = task.Modified // Apply attributes in the order they were specified (important for relative references) dateKeys := DateKeys for _, key := range m.AttributeOrder { valuePtr := m.SetAttributes[key] // Handle date attributes with relative expression support if dateKeys[key] { if err := applyDateAttribute(key, valuePtr, task, resolvedDates); err != nil { return err } continue } // Handle non-date attributes switch key { case "priority": if valuePtr == nil { task.Priority = PriorityDefault } else { task.Priority = Priority(priorityStringToInt(*valuePtr)) } case "project": task.Project = valuePtr case "recur": if valuePtr == nil { task.RecurrenceDuration = nil } else { duration, err := ParseRecurrencePattern(*valuePtr) if err != nil { return fmt.Errorf("invalid recurrence: %w", err) } task.RecurrenceDuration = &duration } } } // Apply tag changes for _, tag := range m.AddTags { if err := task.AddTag(tag); err != nil { return err } } for _, tag := range m.RemoveTags { if err := task.RemoveTag(tag); err != nil { return err } } task.Modified = timeNow() return task.Save() } // ApplyToNew applies modifier to a new task (before it's saved) // This is used when creating tasks with modifiers func (m *Modifier) ApplyToNew(task *Task) error { // Track resolved dates for relative references resolvedDates := make(map[string]time.Time) // Initialize with existing task dates (usually empty for new tasks) if task.Created.IsZero() { resolvedDates["created"] = timeNow() } else { resolvedDates["created"] = task.Created } // Apply attributes in the order they were specified (important for relative references) dateKeys := DateKeys for _, key := range m.AttributeOrder { valuePtr := m.SetAttributes[key] // Handle date attributes with relative expression support if dateKeys[key] { if err := applyDateAttribute(key, valuePtr, task, resolvedDates); err != nil { return err } continue } // Handle non-date attributes switch key { case "priority": if valuePtr == nil { task.Priority = PriorityDefault } else { task.Priority = Priority(priorityStringToInt(*valuePtr)) } case "project": task.Project = valuePtr case "recur": if valuePtr == nil { task.RecurrenceDuration = nil } else { duration, err := ParseRecurrencePattern(*valuePtr) if err != nil { return fmt.Errorf("invalid recurrence: %w", err) } task.RecurrenceDuration = &duration } } } // Note: Tags are added after task is saved (in CreateTask function) return nil } // parseRelativeExpression checks if a string is a relative date expression // Returns: baseAttr, operator, offset, isRelative // Example: "due-1d" -> "due", "-", "1d", true func parseRelativeExpression(s string) (string, string, string, bool) { // Pattern: // Examples: due-1d, scheduled+2w, wait-3days re := regexp.MustCompile(`^([a-z_]+)([\+\-])(.+)$`) matches := re.FindStringSubmatch(s) if len(matches) == 4 { return matches[1], matches[2], matches[3], true } return "", "", "", false } // resolveDateValue resolves a date value, which may be absolute or relative // resolvedDates contains dates that have been resolved so far (for relative references) func resolveDateValue(valueStr string, resolvedDates map[string]time.Time) (time.Time, error) { // Check if it's a relative expression if baseAttr, op, offsetStr, isRelative := parseRelativeExpression(valueStr); isRelative { // Look up the base date baseDate, exists := resolvedDates[baseAttr] if !exists || baseDate.IsZero() { return time.Time{}, fmt.Errorf("cannot reference '%s' in '%s' before it's set", baseAttr, valueStr) } // Parse the offset duration duration, err := ParseRecurrencePattern(offsetStr) if err != nil { return time.Time{}, fmt.Errorf("invalid offset in '%s': %w", valueStr, err) } // Apply the operation if op == "+" { return baseDate.Add(duration), nil } else { return baseDate.Add(-duration), nil } } // Not relative, parse as absolute date return ParseDate(valueStr) } // applyDateAttribute applies a date attribute with support for relative expressions func applyDateAttribute(key string, valuePtr *string, task *Task, resolvedDates map[string]time.Time) error { if valuePtr == nil { // Clear the date switch key { case "due": task.Due = nil case "scheduled": task.Scheduled = nil case "wait": task.Wait = nil case "until": task.Until = nil } return nil } // Resolve the date (may be relative) parsed, err := resolveDateValue(*valuePtr, resolvedDates) if err != nil { return fmt.Errorf("invalid %s date: %w", key, err) } // Set the date on the task switch key { case "due": task.Due = &parsed case "scheduled": task.Scheduled = &parsed case "wait": task.Wait = &parsed case "until": task.Until = &parsed } // Store in resolved dates for future relative references resolvedDates[key] = parsed return nil }