cli + tui to publish to leaflet (wip) & manage tasks, notes & watch/read lists 馃崈
charm leaflet readability golang
29
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 218 lines 5.3 kB view raw
1package handlers 2 3import ( 4 "encoding/json" 5 "fmt" 6 "strings" 7 "time" 8 9 "github.com/stormlightlabs/noteleaf/internal/models" 10 "golang.org/x/text/feature/plural" 11 "golang.org/x/text/language" 12) 13 14// ParsedTaskData holds extracted metadata from a task description 15type ParsedTaskData struct { 16 Description string 17 Project string 18 Context string 19 Tags []string 20 Due string 21 Wait string 22 Scheduled string 23 Recur string 24 Until string 25 ParentUUID string 26 DependsOn []string 27} 28 29// parseDescription extracts inline metadata from description text 30// Supports: +project @context #tag due:YYYY-MM-DD wait:YYYY-MM-DD scheduled:YYYY-MM-DD recur:RULE until:DATE parent:UUID depends:UUID1,UUID2 31func parseDescription(text string) *ParsedTaskData { 32 parsed := &ParsedTaskData{Tags: []string{}, DependsOn: []string{}} 33 words := strings.Fields(text) 34 35 var descWords []string 36 for _, word := range words { 37 switch { 38 case strings.HasPrefix(word, "+"): 39 parsed.Project = strings.TrimPrefix(word, "+") 40 case strings.HasPrefix(word, "@"): 41 parsed.Context = strings.TrimPrefix(word, "@") 42 case strings.HasPrefix(word, "#"): 43 parsed.Tags = append(parsed.Tags, strings.TrimPrefix(word, "#")) 44 case strings.HasPrefix(word, "due:"): 45 parsed.Due = strings.TrimPrefix(word, "due:") 46 case strings.HasPrefix(word, "wait:"): 47 parsed.Wait = strings.TrimPrefix(word, "wait:") 48 case strings.HasPrefix(word, "scheduled:"): 49 parsed.Scheduled = strings.TrimPrefix(word, "scheduled:") 50 case strings.HasPrefix(word, "recur:"): 51 parsed.Recur = strings.TrimPrefix(word, "recur:") 52 case strings.HasPrefix(word, "until:"): 53 parsed.Until = strings.TrimPrefix(word, "until:") 54 case strings.HasPrefix(word, "parent:"): 55 parsed.ParentUUID = strings.TrimPrefix(word, "parent:") 56 case strings.HasPrefix(word, "depends:"): 57 deps := strings.TrimPrefix(word, "depends:") 58 parsed.DependsOn = strings.Split(deps, ",") 59 default: 60 descWords = append(descWords, word) 61 } 62 } 63 64 parsed.Description = strings.Join(descWords, " ") 65 return parsed 66} 67 68func removeString(slice []string, item string) []string { 69 var result []string 70 for _, s := range slice { 71 if s != item { 72 result = append(result, s) 73 } 74 } 75 return result 76} 77 78func pluralize(count int) string { 79 rule := plural.Cardinal.MatchPlural(language.English, count, 0, 0, 0, 0) 80 switch rule { 81 case plural.One: 82 return "" 83 default: 84 return "s" 85 } 86} 87 88func formatDuration(d time.Duration) string { 89 if d < time.Minute { 90 return fmt.Sprintf("%.0fs", d.Seconds()) 91 } 92 if d < time.Hour { 93 return fmt.Sprintf("%.0fm", d.Minutes()) 94 } 95 hours := d.Hours() 96 if hours < 24 { 97 return fmt.Sprintf("%.1fh", hours) 98 } 99 days := int(hours / 24) 100 if remainingHours := hours - float64(days*24); remainingHours == 0 { 101 return fmt.Sprintf("%dd", days) 102 } else { 103 return fmt.Sprintf("%dd %.1fh", days, remainingHours) 104 } 105} 106 107func printTask(task *models.Task) { 108 fmt.Printf("[%d] %s", task.ID, task.Description) 109 110 if task.Status != "pending" { 111 fmt.Printf(" (%s)", task.Status) 112 } 113 114 if task.Priority != "" { 115 fmt.Printf(" [%s]", task.Priority) 116 } 117 118 if task.Project != "" { 119 fmt.Printf(" +%s", task.Project) 120 } 121 122 if task.Context != "" { 123 fmt.Printf(" @%s", task.Context) 124 } 125 126 if len(task.Tags) > 0 { 127 fmt.Printf(" #%s", strings.Join(task.Tags, " #")) 128 } 129 130 if task.Due != nil { 131 fmt.Printf(" (due: %s)", task.Due.Format("2006-01-02")) 132 } 133 134 if task.Recur != "" { 135 fmt.Printf(" \u21bb") 136 } 137 138 if len(task.DependsOn) > 0 { 139 fmt.Printf(" \u2937%d", len(task.DependsOn)) 140 } 141 142 fmt.Println() 143} 144 145func printTaskDetail(task *models.Task, noMetadata bool) { 146 fmt.Printf("Task ID: %d\n", task.ID) 147 fmt.Printf("UUID: %s\n", task.UUID) 148 fmt.Printf("Description: %s\n", task.Description) 149 fmt.Printf("Status: %s\n", task.Status) 150 151 if task.Priority != "" { 152 fmt.Printf("Priority: %s\n", task.Priority) 153 } 154 155 if task.Project != "" { 156 fmt.Printf("Project: %s\n", task.Project) 157 } 158 159 if task.Context != "" { 160 fmt.Printf("Context: %s\n", task.Context) 161 } 162 163 if len(task.Tags) > 0 { 164 fmt.Printf("Tags: %s\n", strings.Join(task.Tags, ", ")) 165 } 166 167 if task.Due != nil { 168 fmt.Printf("Due: %s\n", task.Due.Format("2006-01-02 15:04")) 169 } 170 171 if task.Recur != "" { 172 fmt.Printf("Recurrence: %s\n", task.Recur) 173 } 174 175 if task.Until != nil { 176 fmt.Printf("Recur Until: %s\n", task.Until.Format("2006-01-02")) 177 } 178 179 if task.ParentUUID != nil { 180 fmt.Printf("Parent Task: %s\n", *task.ParentUUID) 181 } 182 183 if len(task.DependsOn) > 0 { 184 fmt.Printf("Depends On:\n") 185 for _, dep := range task.DependsOn { 186 fmt.Printf(" - %s\n", dep) 187 } 188 } 189 190 if !noMetadata { 191 fmt.Printf("Created: %s\n", task.Entry.Format("2006-01-02 15:04")) 192 fmt.Printf("Modified: %s\n", task.Modified.Format("2006-01-02 15:04")) 193 194 if task.Start != nil { 195 fmt.Printf("Started: %s\n", task.Start.Format("2006-01-02 15:04")) 196 } 197 198 if task.End != nil { 199 fmt.Printf("Completed: %s\n", task.End.Format("2006-01-02 15:04")) 200 } 201 } 202 203 if len(task.Annotations) > 0 { 204 fmt.Printf("Annotations:\n") 205 for _, annotation := range task.Annotations { 206 fmt.Printf(" - %s\n", annotation) 207 } 208 } 209} 210 211func printTaskJSON(task *models.Task) error { 212 if data, err := json.MarshalIndent(task, "", " "); err != nil { 213 return fmt.Errorf("failed to marshal task to JSON: %w", err) 214 } else { 215 fmt.Println(string(data)) 216 return nil 217 } 218}