···1+package main
2+3+import (
4+ "testing"
5+6+ "github.com/spf13/cobra"
7+)
8+9+func TestTaskFlags(t *testing.T) {
10+ t.Run("AddCommonTaskFlags", func(t *testing.T) {
11+ cmd := &cobra.Command{}
12+ addCommonTaskFlags(cmd)
13+14+ if cmd.Flags().Lookup("priority") == nil {
15+ t.Error("Expected priority flag to be defined")
16+ }
17+ if cmd.Flags().Lookup("project") == nil {
18+ t.Error("Expected project flag to be defined")
19+ }
20+ if cmd.Flags().Lookup("context") == nil {
21+ t.Error("Expected context flag to be defined")
22+ }
23+ if cmd.Flags().Lookup("tags") == nil {
24+ t.Error("Expected tags flag to be defined")
25+ }
26+27+ if cmd.Flags().ShorthandLookup("p") == nil {
28+ t.Error("Expected 'p' shorthand for priority")
29+ }
30+ if cmd.Flags().ShorthandLookup("c") == nil {
31+ t.Error("Expected 'c' shorthand for context")
32+ }
33+ if cmd.Flags().ShorthandLookup("t") == nil {
34+ t.Error("Expected 't' shorthand for tags")
35+ }
36+ })
37+38+ t.Run("AddRecurrenceFlags", func(t *testing.T) {
39+ cmd := &cobra.Command{}
40+ addRecurrenceFlags(cmd)
41+42+ if cmd.Flags().Lookup("recur") == nil {
43+ t.Error("Expected recur flag to be defined")
44+ }
45+ if cmd.Flags().Lookup("until") == nil {
46+ t.Error("Expected until flag to be defined")
47+ }
48+ })
49+50+ t.Run("AddDependencyFlags", func(t *testing.T) {
51+ cmd := &cobra.Command{}
52+ addDependencyFlags(cmd)
53+54+ if cmd.Flags().Lookup("depends-on") == nil {
55+ t.Error("Expected depends-on flag to be defined")
56+ }
57+ })
58+59+ t.Run("AddParentFlag", func(t *testing.T) {
60+ cmd := &cobra.Command{}
61+ addParentFlag(cmd)
62+63+ if cmd.Flags().Lookup("parent") == nil {
64+ t.Error("Expected parent flag to be defined")
65+ }
66+ })
67+68+ t.Run("AddOutputFlags", func(t *testing.T) {
69+ cmd := &cobra.Command{}
70+ addOutputFlags(cmd)
71+72+ if cmd.Flags().Lookup("format") == nil {
73+ t.Error("Expected format flag to be defined")
74+ }
75+ if cmd.Flags().Lookup("json") == nil {
76+ t.Error("Expected json flag to be defined")
77+ }
78+ if cmd.Flags().Lookup("no-metadata") == nil {
79+ t.Error("Expected no-metadata flag to be defined")
80+ }
81+82+ format, _ := cmd.Flags().GetString("format")
83+ if format != "detailed" {
84+ t.Errorf("Expected format default to be 'detailed', got '%s'", format)
85+ }
86+ })
87+88+ t.Run("AddDueDateFlag", func(t *testing.T) {
89+ cmd := &cobra.Command{}
90+ addDueDateFlag(cmd)
91+92+ if cmd.Flags().Lookup("due") == nil {
93+ t.Error("Expected due flag to be defined")
94+ }
95+96+ if cmd.Flags().ShorthandLookup("d") == nil {
97+ t.Error("Expected 'd' shorthand for due")
98+ }
99+ })
100+}
+37
docs/manual/noteleaf.1.txt
···113 -d, --days <n> Number of days (default 7)
114 -t, --task <id> Timesheet for specific task
11500000000000000000000000000116 MOVIE COMMANDS
117 noteleaf movie add [query...]
118 Search and add a movie to the watch queue.
···247 Save an article:
248 noteleaf article add https://example.com/post
249 noteleaf article list --author "Ada Lovelace"
00000000000250251FILES
252 (TODO: configuration and data file paths once implemented)
···113 -d, --days <n> Number of days (default 7)
114 -t, --task <id> Timesheet for specific task
115116+ noteleaf task recur set <id>
117+ Set recurrence rule for a task.
118+ Flags:
119+ --rule <value> Recurrence rule (e.g., FREQ=DAILY)
120+ --until YYYY-MM-DD Recurrence end date
121+122+ noteleaf task recur clear <id>
123+ Clear recurrence rule from a task.
124+125+ noteleaf task recur show <id>
126+ Show recurrence details for a task.
127+128+ noteleaf task depend add <id> <depends-on-uuid>
129+ Add a dependency to a task.
130+131+ noteleaf task depend remove <id> <depends-on-uuid>
132+ Remove a dependency from a task.
133+ Alias: rm
134+135+ noteleaf task depend list <id>
136+ List dependencies for a task.
137+ Alias: ls
138+139+ noteleaf task depend blocked-by <id>
140+ Show tasks blocked by this task.
141+142 MOVIE COMMANDS
143 noteleaf movie add [query...]
144 Search and add a movie to the watch queue.
···273 Save an article:
274 noteleaf article add https://example.com/post
275 noteleaf article list --author "Ada Lovelace"
276+277+ Manage task recurrence:
278+ noteleaf task recur set 42 --rule FREQ=DAILY --until 2025-12-31
279+ noteleaf task recur show 42
280+ noteleaf task recur clear 42
281+282+ Manage task dependencies:
283+ noteleaf task depend add 42 abc123-uuid
284+ noteleaf task depend list 42
285+ noteleaf task depend blocked-by abc123-uuid
286+ noteleaf task depend remove 42 abc123-uuid
287288FILES
289 (TODO: configuration and data file paths once implemented)
+230-6
internal/handlers/tasks.go
···261 task.Tags = removeString(task.Tags, tag)
262 }
263264- // Handle dependency additions
265 if addDeps != "" {
266- deps := strings.Split(addDeps, ",")
267- for _, dep := range deps {
268 dep = strings.TrimSpace(dep)
269 if dep != "" && !slices.Contains(task.DependsOn, dep) {
270 task.DependsOn = append(task.DependsOn, dep)
···272 }
273 }
274275- // Handle dependency removals
276 if removeDeps != "" {
277- deps := strings.Split(removeDeps, ",")
278- for _, dep := range deps {
279 dep = strings.TrimSpace(dep)
280 task.DependsOn = removeString(task.DependsOn, dep)
281 }
···848 return fmt.Errorf("failed to marshal task to JSON: %w", err)
849 }
850 fmt.Println(string(jsonData))
0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000851 return nil
852}
853
···261 task.Tags = removeString(task.Tags, tag)
262 }
2630264 if addDeps != "" {
265+ deps := strings.SplitSeq(addDeps, ",")
266+ for dep := range deps {
267 dep = strings.TrimSpace(dep)
268 if dep != "" && !slices.Contains(task.DependsOn, dep) {
269 task.DependsOn = append(task.DependsOn, dep)
···271 }
272 }
2730274 if removeDeps != "" {
275+ deps := strings.SplitSeq(removeDeps, ",")
276+ for dep := range deps {
277 dep = strings.TrimSpace(dep)
278 task.DependsOn = removeString(task.DependsOn, dep)
279 }
···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
855+ var err error
856+857+ if id, err_ := strconv.ParseInt(taskID, 10, 64); err_ == nil {
858+ task, err = h.repos.Tasks.Get(ctx, id)
859+ } else {
860+ task, err = h.repos.Tasks.GetByUUID(ctx, taskID)
861+ }
862+863+ if err != nil {
864+ return fmt.Errorf("failed to find task: %w", err)
865+ }
866+867+ if rule != "" {
868+ task.Recur = models.RRule(rule)
869+ }
870+871+ if until != "" {
872+ if untilTime, err := time.Parse("2006-01-02", until); err == nil {
873+ task.Until = &untilTime
874+ } else {
875+ return fmt.Errorf("invalid until date format, use YYYY-MM-DD: %w", err)
876+ }
877+ }
878+879+ err = h.repos.Tasks.Update(ctx, task)
880+ if err != nil {
881+ return fmt.Errorf("failed to update task recurrence: %w", err)
882+ }
883+884+ fmt.Printf("Recurrence set for task (ID: %d): %s\n", task.ID, task.Description)
885+ if task.Recur != "" {
886+ fmt.Printf("Rule: %s\n", task.Recur)
887+ }
888+ if task.Until != nil {
889+ fmt.Printf("Until: %s\n", task.Until.Format("2006-01-02"))
890+ }
891+892+ return nil
893+}
894+895+// ClearRecur clears the recurrence rule from a task
896+func (h *TaskHandler) ClearRecur(ctx context.Context, taskID string) error {
897+ var task *models.Task
898+ var err error
899+900+ if id, err_ := strconv.ParseInt(taskID, 10, 64); err_ == nil {
901+ task, err = h.repos.Tasks.Get(ctx, id)
902+ } else {
903+ task, err = h.repos.Tasks.GetByUUID(ctx, taskID)
904+ }
905+906+ if err != nil {
907+ return fmt.Errorf("failed to find task: %w", err)
908+ }
909+910+ task.Recur = ""
911+ task.Until = nil
912+913+ err = h.repos.Tasks.Update(ctx, task)
914+ if err != nil {
915+ return fmt.Errorf("failed to clear task recurrence: %w", err)
916+ }
917+918+ fmt.Printf("Recurrence cleared for task (ID: %d): %s\n", task.ID, task.Description)
919+ return nil
920+}
921+922+// ShowRecur displays the recurrence details for a task
923+func (h *TaskHandler) ShowRecur(ctx context.Context, taskID string) error {
924+ var task *models.Task
925+ var err error
926+927+ if id, err_ := strconv.ParseInt(taskID, 10, 64); err_ == nil {
928+ task, err = h.repos.Tasks.Get(ctx, id)
929+ } else {
930+ task, err = h.repos.Tasks.GetByUUID(ctx, taskID)
931+ }
932+933+ if err != nil {
934+ return fmt.Errorf("failed to find task: %w", err)
935+ }
936+937+ fmt.Printf("Task (ID: %d): %s\n", task.ID, task.Description)
938+ if task.Recur != "" {
939+ fmt.Printf("Recurrence rule: %s\n", task.Recur)
940+ if task.Until != nil {
941+ fmt.Printf("Recurrence until: %s\n", task.Until.Format("2006-01-02"))
942+ } else {
943+ fmt.Printf("Recurrence until: (no end date)\n")
944+ }
945+ } else {
946+ fmt.Printf("No recurrence set\n")
947+ }
948+949+ return nil
950+}
951+952+// AddDep adds a dependency to a task
953+func (h *TaskHandler) AddDep(ctx context.Context, taskID, dependsOnUUID string) error {
954+ var task *models.Task
955+ var err error
956+957+ if id, err_ := strconv.ParseInt(taskID, 10, 64); err_ == nil {
958+ task, err = h.repos.Tasks.Get(ctx, id)
959+ } else {
960+ task, err = h.repos.Tasks.GetByUUID(ctx, taskID)
961+ }
962+963+ if err != nil {
964+ return fmt.Errorf("failed to find task: %w", err)
965+ }
966+967+ if _, err := h.repos.Tasks.GetByUUID(ctx, dependsOnUUID); err != nil {
968+ return fmt.Errorf("dependency task not found: %w", err)
969+ }
970+971+ err = h.repos.Tasks.AddDependency(ctx, task.UUID, dependsOnUUID)
972+ if err != nil {
973+ return fmt.Errorf("failed to add dependency: %w", err)
974+ }
975+976+ fmt.Printf("Dependency added to task (ID: %d): %s\n", task.ID, task.Description)
977+ fmt.Printf("Now depends on: %s\n", dependsOnUUID)
978+979+ return nil
980+}
981+982+// RemoveDep removes a dependency from a task
983+func (h *TaskHandler) RemoveDep(ctx context.Context, taskID, dependsOnUUID string) error {
984+ var task *models.Task
985+ var err error
986+987+ if id, err_ := strconv.ParseInt(taskID, 10, 64); err_ == nil {
988+ task, err = h.repos.Tasks.Get(ctx, id)
989+ } else {
990+ task, err = h.repos.Tasks.GetByUUID(ctx, taskID)
991+ }
992+993+ if err != nil {
994+ return fmt.Errorf("failed to find task: %w", err)
995+ }
996+997+ err = h.repos.Tasks.RemoveDependency(ctx, task.UUID, dependsOnUUID)
998+ if err != nil {
999+ return fmt.Errorf("failed to remove dependency: %w", err)
1000+ }
1001+1002+ fmt.Printf("Dependency removed from task (ID: %d): %s\n", task.ID, task.Description)
1003+ fmt.Printf("No longer depends on: %s\n", dependsOnUUID)
1004+1005+ return nil
1006+}
1007+1008+// ListDeps lists all dependencies for a task
1009+func (h *TaskHandler) ListDeps(ctx context.Context, taskID string) error {
1010+ var task *models.Task
1011+ var err error
1012+1013+ if id, err_ := strconv.ParseInt(taskID, 10, 64); err_ == nil {
1014+ task, err = h.repos.Tasks.Get(ctx, id)
1015+ } else {
1016+ task, err = h.repos.Tasks.GetByUUID(ctx, taskID)
1017+ }
1018+1019+ if err != nil {
1020+ return fmt.Errorf("failed to find task: %w", err)
1021+ }
1022+1023+ fmt.Printf("Task (ID: %d): %s\n", task.ID, task.Description)
1024+1025+ if len(task.DependsOn) == 0 {
1026+ fmt.Printf("No dependencies\n")
1027+ return nil
1028+ }
1029+1030+ fmt.Printf("Depends on %d task(s):\n", len(task.DependsOn))
1031+ for _, depUUID := range task.DependsOn {
1032+ depTask, err := h.repos.Tasks.GetByUUID(ctx, depUUID)
1033+ if err != nil {
1034+ fmt.Printf(" - %s (not found)\n", depUUID)
1035+ continue
1036+ }
1037+ fmt.Printf(" - [%d] %s (UUID: %s)\n", depTask.ID, depTask.Description, depTask.UUID)
1038+ }
1039+1040+ return nil
1041+}
1042+1043+// BlockedByDep shows tasks that are blocked by the given task
1044+func (h *TaskHandler) BlockedByDep(ctx context.Context, taskID string) error {
1045+ var task *models.Task
1046+ var err error
1047+1048+ if id, err_ := strconv.ParseInt(taskID, 10, 64); err_ == nil {
1049+ task, err = h.repos.Tasks.Get(ctx, id)
1050+ } else {
1051+ task, err = h.repos.Tasks.GetByUUID(ctx, taskID)
1052+ }
1053+1054+ if err != nil {
1055+ return fmt.Errorf("failed to find task: %w", err)
1056+ }
1057+1058+ fmt.Printf("Task (ID: %d): %s\n", task.ID, task.Description)
1059+1060+ dependents, err := h.repos.Tasks.GetDependents(ctx, task.UUID)
1061+ if err != nil {
1062+ return fmt.Errorf("failed to get dependent tasks: %w", err)
1063+ }
1064+1065+ if len(dependents) == 0 {
1066+ fmt.Printf("No tasks are blocked by this task\n")
1067+ return nil
1068+ }
1069+1070+ fmt.Printf("Blocks %d task(s):\n", len(dependents))
1071+ for _, dep := range dependents {
1072+ fmt.Printf(" - [%d] %s\n", dep.ID, dep.Description)
1073+ }
1074+1075 return nil
1076}
1077