tangled
alpha
login
or
join now
desertthunder.dev
/
noteleaf
cli + tui to publish to leaflet (wip) & manage tasks, notes & watch/read lists ๐
charm
leaflet
readability
golang
29
fork
atom
overview
issues
2
pulls
pipelines
refactor: pull out task helpers in handlers pkg
desertthunder.dev
4 months ago
5c2dd115
f05cd7c8
+218
-209
3 changed files
expand all
collapse all
unified
split
internal
handlers
task_helpers.go
tasks.go
tasks_test.go
+212
internal/handlers/task_helpers.go
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
package handlers
2
+
3
+
import (
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
15
+
type ParsedTaskData struct {
16
+
Description string
17
+
Project string
18
+
Context string
19
+
Tags []string
20
+
Due string
21
+
Recur string
22
+
Until string
23
+
ParentUUID string
24
+
DependsOn []string
25
+
}
26
+
27
+
// parseDescription extracts inline metadata from description text
28
+
// Supports: +project @context #tag due:YYYY-MM-DD recur:RULE until:DATE parent:UUID depends:UUID1,UUID2
29
+
func parseDescription(text string) *ParsedTaskData {
30
+
parsed := &ParsedTaskData{Tags: []string{}, DependsOn: []string{}}
31
+
words := strings.Fields(text)
32
+
33
+
var descWords []string
34
+
for _, word := range words {
35
+
switch {
36
+
case strings.HasPrefix(word, "+"):
37
+
parsed.Project = strings.TrimPrefix(word, "+")
38
+
case strings.HasPrefix(word, "@"):
39
+
parsed.Context = strings.TrimPrefix(word, "@")
40
+
case strings.HasPrefix(word, "#"):
41
+
parsed.Tags = append(parsed.Tags, strings.TrimPrefix(word, "#"))
42
+
case strings.HasPrefix(word, "due:"):
43
+
parsed.Due = strings.TrimPrefix(word, "due:")
44
+
case strings.HasPrefix(word, "recur:"):
45
+
parsed.Recur = strings.TrimPrefix(word, "recur:")
46
+
case strings.HasPrefix(word, "until:"):
47
+
parsed.Until = strings.TrimPrefix(word, "until:")
48
+
case strings.HasPrefix(word, "parent:"):
49
+
parsed.ParentUUID = strings.TrimPrefix(word, "parent:")
50
+
case strings.HasPrefix(word, "depends:"):
51
+
deps := strings.TrimPrefix(word, "depends:")
52
+
parsed.DependsOn = strings.Split(deps, ",")
53
+
default:
54
+
descWords = append(descWords, word)
55
+
}
56
+
}
57
+
58
+
parsed.Description = strings.Join(descWords, " ")
59
+
return parsed
60
+
}
61
+
62
+
func removeString(slice []string, item string) []string {
63
+
var result []string
64
+
for _, s := range slice {
65
+
if s != item {
66
+
result = append(result, s)
67
+
}
68
+
}
69
+
return result
70
+
}
71
+
72
+
func pluralize(count int) string {
73
+
rule := plural.Cardinal.MatchPlural(language.English, count, 0, 0, 0, 0)
74
+
switch rule {
75
+
case plural.One:
76
+
return ""
77
+
default:
78
+
return "s"
79
+
}
80
+
}
81
+
82
+
func formatDuration(d time.Duration) string {
83
+
if d < time.Minute {
84
+
return fmt.Sprintf("%.0fs", d.Seconds())
85
+
}
86
+
if d < time.Hour {
87
+
return fmt.Sprintf("%.0fm", d.Minutes())
88
+
}
89
+
hours := d.Hours()
90
+
if hours < 24 {
91
+
return fmt.Sprintf("%.1fh", hours)
92
+
}
93
+
days := int(hours / 24)
94
+
if remainingHours := hours - float64(days*24); remainingHours == 0 {
95
+
return fmt.Sprintf("%dd", days)
96
+
} else {
97
+
return fmt.Sprintf("%dd %.1fh", days, remainingHours)
98
+
}
99
+
}
100
+
101
+
func printTask(task *models.Task) {
102
+
fmt.Printf("[%d] %s", task.ID, task.Description)
103
+
104
+
if task.Status != "pending" {
105
+
fmt.Printf(" (%s)", task.Status)
106
+
}
107
+
108
+
if task.Priority != "" {
109
+
fmt.Printf(" [%s]", task.Priority)
110
+
}
111
+
112
+
if task.Project != "" {
113
+
fmt.Printf(" +%s", task.Project)
114
+
}
115
+
116
+
if task.Context != "" {
117
+
fmt.Printf(" @%s", task.Context)
118
+
}
119
+
120
+
if len(task.Tags) > 0 {
121
+
fmt.Printf(" #%s", strings.Join(task.Tags, " #"))
122
+
}
123
+
124
+
if task.Due != nil {
125
+
fmt.Printf(" (due: %s)", task.Due.Format("2006-01-02"))
126
+
}
127
+
128
+
if task.Recur != "" {
129
+
fmt.Printf(" \u21bb")
130
+
}
131
+
132
+
if len(task.DependsOn) > 0 {
133
+
fmt.Printf(" \u2937%d", len(task.DependsOn))
134
+
}
135
+
136
+
fmt.Println()
137
+
}
138
+
139
+
func printTaskDetail(task *models.Task, noMetadata bool) {
140
+
fmt.Printf("Task ID: %d\n", task.ID)
141
+
fmt.Printf("UUID: %s\n", task.UUID)
142
+
fmt.Printf("Description: %s\n", task.Description)
143
+
fmt.Printf("Status: %s\n", task.Status)
144
+
145
+
if task.Priority != "" {
146
+
fmt.Printf("Priority: %s\n", task.Priority)
147
+
}
148
+
149
+
if task.Project != "" {
150
+
fmt.Printf("Project: %s\n", task.Project)
151
+
}
152
+
153
+
if task.Context != "" {
154
+
fmt.Printf("Context: %s\n", task.Context)
155
+
}
156
+
157
+
if len(task.Tags) > 0 {
158
+
fmt.Printf("Tags: %s\n", strings.Join(task.Tags, ", "))
159
+
}
160
+
161
+
if task.Due != nil {
162
+
fmt.Printf("Due: %s\n", task.Due.Format("2006-01-02 15:04"))
163
+
}
164
+
165
+
if task.Recur != "" {
166
+
fmt.Printf("Recurrence: %s\n", task.Recur)
167
+
}
168
+
169
+
if task.Until != nil {
170
+
fmt.Printf("Recur Until: %s\n", task.Until.Format("2006-01-02"))
171
+
}
172
+
173
+
if task.ParentUUID != nil {
174
+
fmt.Printf("Parent Task: %s\n", *task.ParentUUID)
175
+
}
176
+
177
+
if len(task.DependsOn) > 0 {
178
+
fmt.Printf("Depends On:\n")
179
+
for _, dep := range task.DependsOn {
180
+
fmt.Printf(" - %s\n", dep)
181
+
}
182
+
}
183
+
184
+
if !noMetadata {
185
+
fmt.Printf("Created: %s\n", task.Entry.Format("2006-01-02 15:04"))
186
+
fmt.Printf("Modified: %s\n", task.Modified.Format("2006-01-02 15:04"))
187
+
188
+
if task.Start != nil {
189
+
fmt.Printf("Started: %s\n", task.Start.Format("2006-01-02 15:04"))
190
+
}
191
+
192
+
if task.End != nil {
193
+
fmt.Printf("Completed: %s\n", task.End.Format("2006-01-02 15:04"))
194
+
}
195
+
}
196
+
197
+
if len(task.Annotations) > 0 {
198
+
fmt.Printf("Annotations:\n")
199
+
for _, annotation := range task.Annotations {
200
+
fmt.Printf(" - %s\n", annotation)
201
+
}
202
+
}
203
+
}
204
+
205
+
func printTaskJSON(task *models.Task) error {
206
+
if data, err := json.MarshalIndent(task, "", " "); err != nil {
207
+
return fmt.Errorf("failed to marshal task to JSON: %w", err)
208
+
} else {
209
+
fmt.Println(string(data))
210
+
return nil
211
+
}
212
+
}
+4
-207
internal/handlers/tasks.go
···
2
3
import (
4
"context"
5
-
"encoding/json"
6
"fmt"
7
"os"
8
"slices"
···
15
"github.com/stormlightlabs/noteleaf/internal/repo"
16
"github.com/stormlightlabs/noteleaf/internal/store"
17
"github.com/stormlightlabs/noteleaf/internal/ui"
18
-
"golang.org/x/text/feature/plural"
19
-
"golang.org/x/text/language"
20
)
21
22
// TaskHandler handles all task-related commands
···
188
189
fmt.Printf("Found %d task(s):\n\n", len(tasks))
190
for _, task := range tasks {
191
-
h.printTask(task)
192
}
193
194
return nil
···
380
}
381
382
if jsonOutput {
383
-
return h.printTaskJSON(task)
384
}
385
386
if format == "brief" {
387
-
h.printTask(task)
388
} else {
389
-
h.printTaskDetail(task, noMetadata)
390
}
391
return nil
392
}
···
736
return tagTable.Browse(ctx)
737
}
738
739
-
func (h *TaskHandler) printTask(task *models.Task) {
740
-
fmt.Printf("[%d] %s", task.ID, task.Description)
741
-
742
-
if task.Status != "pending" {
743
-
fmt.Printf(" (%s)", task.Status)
744
-
}
745
-
746
-
if task.Priority != "" {
747
-
fmt.Printf(" [%s]", task.Priority)
748
-
}
749
-
750
-
if task.Project != "" {
751
-
fmt.Printf(" +%s", task.Project)
752
-
}
753
-
754
-
if task.Context != "" {
755
-
fmt.Printf(" @%s", task.Context)
756
-
}
757
-
758
-
if len(task.Tags) > 0 {
759
-
fmt.Printf(" #%s", strings.Join(task.Tags, " #"))
760
-
}
761
-
762
-
if task.Due != nil {
763
-
fmt.Printf(" (due: %s)", task.Due.Format("2006-01-02"))
764
-
}
765
-
766
-
if task.Recur != "" {
767
-
fmt.Printf(" \u21bb")
768
-
}
769
-
770
-
if len(task.DependsOn) > 0 {
771
-
fmt.Printf(" \u2937%d", len(task.DependsOn))
772
-
}
773
-
774
-
fmt.Println()
775
-
}
776
-
777
-
func (h *TaskHandler) printTaskDetail(task *models.Task, noMetadata bool) {
778
-
fmt.Printf("Task ID: %d\n", task.ID)
779
-
fmt.Printf("UUID: %s\n", task.UUID)
780
-
fmt.Printf("Description: %s\n", task.Description)
781
-
fmt.Printf("Status: %s\n", task.Status)
782
-
783
-
if task.Priority != "" {
784
-
fmt.Printf("Priority: %s\n", task.Priority)
785
-
}
786
-
787
-
if task.Project != "" {
788
-
fmt.Printf("Project: %s\n", task.Project)
789
-
}
790
-
791
-
if task.Context != "" {
792
-
fmt.Printf("Context: %s\n", task.Context)
793
-
}
794
-
795
-
if len(task.Tags) > 0 {
796
-
fmt.Printf("Tags: %s\n", strings.Join(task.Tags, ", "))
797
-
}
798
-
799
-
if task.Due != nil {
800
-
fmt.Printf("Due: %s\n", task.Due.Format("2006-01-02 15:04"))
801
-
}
802
-
803
-
if task.Recur != "" {
804
-
fmt.Printf("Recurrence: %s\n", task.Recur)
805
-
}
806
-
807
-
if task.Until != nil {
808
-
fmt.Printf("Recur Until: %s\n", task.Until.Format("2006-01-02"))
809
-
}
810
-
811
-
if task.ParentUUID != nil {
812
-
fmt.Printf("Parent Task: %s\n", *task.ParentUUID)
813
-
}
814
-
815
-
if len(task.DependsOn) > 0 {
816
-
fmt.Printf("Depends On:\n")
817
-
for _, dep := range task.DependsOn {
818
-
fmt.Printf(" - %s\n", dep)
819
-
}
820
-
}
821
-
822
-
if !noMetadata {
823
-
fmt.Printf("Created: %s\n", task.Entry.Format("2006-01-02 15:04"))
824
-
fmt.Printf("Modified: %s\n", task.Modified.Format("2006-01-02 15:04"))
825
-
826
-
if task.Start != nil {
827
-
fmt.Printf("Started: %s\n", task.Start.Format("2006-01-02 15:04"))
828
-
}
829
-
830
-
if task.End != nil {
831
-
fmt.Printf("Completed: %s\n", task.End.Format("2006-01-02 15:04"))
832
-
}
833
-
}
834
-
835
-
if len(task.Annotations) > 0 {
836
-
fmt.Printf("Annotations:\n")
837
-
for _, annotation := range task.Annotations {
838
-
fmt.Printf(" - %s\n", annotation)
839
-
}
840
-
}
841
-
}
842
-
843
-
func (h *TaskHandler) printTaskJSON(task *models.Task) error {
844
-
jsonData, err := json.MarshalIndent(task, "", " ")
845
-
if err != nil {
846
-
return fmt.Errorf("failed to marshal task to JSON: %w", err)
847
-
}
848
-
fmt.Println(string(jsonData))
849
-
return nil
850
-
}
851
-
852
// SetRecur sets the recurrence rule for a task
853
func (h *TaskHandler) SetRecur(ctx context.Context, taskID, rule, until string) error {
854
var task *models.Task
···
1074
1075
return nil
1076
}
1077
-
1078
-
// ParsedTaskData holds extracted metadata from a task description
1079
-
type ParsedTaskData struct {
1080
-
Description string
1081
-
Project string
1082
-
Context string
1083
-
Tags []string
1084
-
Due string
1085
-
Recur string
1086
-
Until string
1087
-
ParentUUID string
1088
-
DependsOn []string
1089
-
}
1090
-
1091
-
// parseDescription extracts inline metadata from description text
1092
-
// Supports: +project @context #tag due:YYYY-MM-DD recur:RULE until:DATE parent:UUID depends:UUID1,UUID2
1093
-
func parseDescription(text string) *ParsedTaskData {
1094
-
parsed := &ParsedTaskData{Tags: []string{}, DependsOn: []string{}}
1095
-
words := strings.Fields(text)
1096
-
1097
-
var descWords []string
1098
-
for _, word := range words {
1099
-
switch {
1100
-
case strings.HasPrefix(word, "+"):
1101
-
parsed.Project = strings.TrimPrefix(word, "+")
1102
-
case strings.HasPrefix(word, "@"):
1103
-
parsed.Context = strings.TrimPrefix(word, "@")
1104
-
case strings.HasPrefix(word, "#"):
1105
-
parsed.Tags = append(parsed.Tags, strings.TrimPrefix(word, "#"))
1106
-
case strings.HasPrefix(word, "due:"):
1107
-
parsed.Due = strings.TrimPrefix(word, "due:")
1108
-
case strings.HasPrefix(word, "recur:"):
1109
-
parsed.Recur = strings.TrimPrefix(word, "recur:")
1110
-
case strings.HasPrefix(word, "until:"):
1111
-
parsed.Until = strings.TrimPrefix(word, "until:")
1112
-
case strings.HasPrefix(word, "parent:"):
1113
-
parsed.ParentUUID = strings.TrimPrefix(word, "parent:")
1114
-
case strings.HasPrefix(word, "depends:"):
1115
-
deps := strings.TrimPrefix(word, "depends:")
1116
-
parsed.DependsOn = strings.Split(deps, ",")
1117
-
default:
1118
-
descWords = append(descWords, word)
1119
-
}
1120
-
}
1121
-
1122
-
parsed.Description = strings.Join(descWords, " ")
1123
-
return parsed
1124
-
}
1125
-
1126
-
func removeString(slice []string, item string) []string {
1127
-
var result []string
1128
-
for _, s := range slice {
1129
-
if s != item {
1130
-
result = append(result, s)
1131
-
}
1132
-
}
1133
-
return result
1134
-
}
1135
-
1136
-
func pluralize(count int) string {
1137
-
rule := plural.Cardinal.MatchPlural(language.English, count, 0, 0, 0, 0)
1138
-
switch rule {
1139
-
case plural.One:
1140
-
return ""
1141
-
default:
1142
-
return "s"
1143
-
}
1144
-
}
1145
-
1146
-
func formatDuration(d time.Duration) string {
1147
-
if d < time.Minute {
1148
-
return fmt.Sprintf("%.0fs", d.Seconds())
1149
-
}
1150
-
if d < time.Hour {
1151
-
return fmt.Sprintf("%.0fm", d.Minutes())
1152
-
}
1153
-
hours := d.Hours()
1154
-
if hours < 24 {
1155
-
return fmt.Sprintf("%.1fh", hours)
1156
-
}
1157
-
days := int(hours / 24)
1158
-
if remainingHours := hours - float64(days*24); remainingHours == 0 {
1159
-
return fmt.Sprintf("%dd", days)
1160
-
} else {
1161
-
return fmt.Sprintf("%dd %.1fh", days, remainingHours)
1162
-
}
1163
-
}
···
2
3
import (
4
"context"
0
5
"fmt"
6
"os"
7
"slices"
···
14
"github.com/stormlightlabs/noteleaf/internal/repo"
15
"github.com/stormlightlabs/noteleaf/internal/store"
16
"github.com/stormlightlabs/noteleaf/internal/ui"
0
0
17
)
18
19
// TaskHandler handles all task-related commands
···
185
186
fmt.Printf("Found %d task(s):\n\n", len(tasks))
187
for _, task := range tasks {
188
+
printTask(task)
189
}
190
191
return nil
···
377
}
378
379
if jsonOutput {
380
+
return printTaskJSON(task)
381
}
382
383
if format == "brief" {
384
+
printTask(task)
385
} else {
386
+
printTaskDetail(task, noMetadata)
387
}
388
return nil
389
}
···
733
return tagTable.Browse(ctx)
734
}
735
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
736
// SetRecur sets the recurrence rule for a task
737
func (h *TaskHandler) SetRecur(ctx context.Context, taskID, rule, until string) error {
738
var task *models.Task
···
958
959
return nil
960
}
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
+2
-2
internal/handlers/tasks_test.go
···
811
}
812
}()
813
814
-
handler.printTask(task)
815
})
816
817
t.Run("printTaskDetail doesn't panic", func(t *testing.T) {
···
821
}
822
}()
823
824
-
handler.printTaskDetail(task, false)
825
})
826
})
827
···
811
}
812
}()
813
814
+
printTask(task)
815
})
816
817
t.Run("printTaskDetail doesn't panic", func(t *testing.T) {
···
821
}
822
}()
823
824
+
printTaskDetail(task, false)
825
})
826
})
827