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