cli + tui to publish to leaflet (wip) & manage tasks, notes & watch/read lists 馃崈
charm leaflet readability golang
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}