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