···11+package main
22+33+import (
44+ "testing"
55+66+ "github.com/spf13/cobra"
77+)
88+99+func TestTaskFlags(t *testing.T) {
1010+ t.Run("AddCommonTaskFlags", func(t *testing.T) {
1111+ cmd := &cobra.Command{}
1212+ addCommonTaskFlags(cmd)
1313+1414+ if cmd.Flags().Lookup("priority") == nil {
1515+ t.Error("Expected priority flag to be defined")
1616+ }
1717+ if cmd.Flags().Lookup("project") == nil {
1818+ t.Error("Expected project flag to be defined")
1919+ }
2020+ if cmd.Flags().Lookup("context") == nil {
2121+ t.Error("Expected context flag to be defined")
2222+ }
2323+ if cmd.Flags().Lookup("tags") == nil {
2424+ t.Error("Expected tags flag to be defined")
2525+ }
2626+2727+ if cmd.Flags().ShorthandLookup("p") == nil {
2828+ t.Error("Expected 'p' shorthand for priority")
2929+ }
3030+ if cmd.Flags().ShorthandLookup("c") == nil {
3131+ t.Error("Expected 'c' shorthand for context")
3232+ }
3333+ if cmd.Flags().ShorthandLookup("t") == nil {
3434+ t.Error("Expected 't' shorthand for tags")
3535+ }
3636+ })
3737+3838+ t.Run("AddRecurrenceFlags", func(t *testing.T) {
3939+ cmd := &cobra.Command{}
4040+ addRecurrenceFlags(cmd)
4141+4242+ if cmd.Flags().Lookup("recur") == nil {
4343+ t.Error("Expected recur flag to be defined")
4444+ }
4545+ if cmd.Flags().Lookup("until") == nil {
4646+ t.Error("Expected until flag to be defined")
4747+ }
4848+ })
4949+5050+ t.Run("AddDependencyFlags", func(t *testing.T) {
5151+ cmd := &cobra.Command{}
5252+ addDependencyFlags(cmd)
5353+5454+ if cmd.Flags().Lookup("depends-on") == nil {
5555+ t.Error("Expected depends-on flag to be defined")
5656+ }
5757+ })
5858+5959+ t.Run("AddParentFlag", func(t *testing.T) {
6060+ cmd := &cobra.Command{}
6161+ addParentFlag(cmd)
6262+6363+ if cmd.Flags().Lookup("parent") == nil {
6464+ t.Error("Expected parent flag to be defined")
6565+ }
6666+ })
6767+6868+ t.Run("AddOutputFlags", func(t *testing.T) {
6969+ cmd := &cobra.Command{}
7070+ addOutputFlags(cmd)
7171+7272+ if cmd.Flags().Lookup("format") == nil {
7373+ t.Error("Expected format flag to be defined")
7474+ }
7575+ if cmd.Flags().Lookup("json") == nil {
7676+ t.Error("Expected json flag to be defined")
7777+ }
7878+ if cmd.Flags().Lookup("no-metadata") == nil {
7979+ t.Error("Expected no-metadata flag to be defined")
8080+ }
8181+8282+ format, _ := cmd.Flags().GetString("format")
8383+ if format != "detailed" {
8484+ t.Errorf("Expected format default to be 'detailed', got '%s'", format)
8585+ }
8686+ })
8787+8888+ t.Run("AddDueDateFlag", func(t *testing.T) {
8989+ cmd := &cobra.Command{}
9090+ addDueDateFlag(cmd)
9191+9292+ if cmd.Flags().Lookup("due") == nil {
9393+ t.Error("Expected due flag to be defined")
9494+ }
9595+9696+ if cmd.Flags().ShorthandLookup("d") == nil {
9797+ t.Error("Expected 'd' shorthand for due")
9898+ }
9999+ })
100100+}
+37
docs/manual/noteleaf.1.txt
···113113 -d, --days <n> Number of days (default 7)
114114 -t, --task <id> Timesheet for specific task
115115116116+ noteleaf task recur set <id>
117117+ Set recurrence rule for a task.
118118+ Flags:
119119+ --rule <value> Recurrence rule (e.g., FREQ=DAILY)
120120+ --until YYYY-MM-DD Recurrence end date
121121+122122+ noteleaf task recur clear <id>
123123+ Clear recurrence rule from a task.
124124+125125+ noteleaf task recur show <id>
126126+ Show recurrence details for a task.
127127+128128+ noteleaf task depend add <id> <depends-on-uuid>
129129+ Add a dependency to a task.
130130+131131+ noteleaf task depend remove <id> <depends-on-uuid>
132132+ Remove a dependency from a task.
133133+ Alias: rm
134134+135135+ noteleaf task depend list <id>
136136+ List dependencies for a task.
137137+ Alias: ls
138138+139139+ noteleaf task depend blocked-by <id>
140140+ Show tasks blocked by this task.
141141+116142 MOVIE COMMANDS
117143 noteleaf movie add [query...]
118144 Search and add a movie to the watch queue.
···247273 Save an article:
248274 noteleaf article add https://example.com/post
249275 noteleaf article list --author "Ada Lovelace"
276276+277277+ Manage task recurrence:
278278+ noteleaf task recur set 42 --rule FREQ=DAILY --until 2025-12-31
279279+ noteleaf task recur show 42
280280+ noteleaf task recur clear 42
281281+282282+ Manage task dependencies:
283283+ noteleaf task depend add 42 abc123-uuid
284284+ noteleaf task depend list 42
285285+ noteleaf task depend blocked-by abc123-uuid
286286+ noteleaf task depend remove 42 abc123-uuid
250287251288FILES
252289 (TODO: configuration and data file paths once implemented)
+230-6
internal/handlers/tasks.go
···261261 task.Tags = removeString(task.Tags, tag)
262262 }
263263264264- // Handle dependency additions
265264 if addDeps != "" {
266266- deps := strings.Split(addDeps, ",")
267267- for _, dep := range deps {
265265+ deps := strings.SplitSeq(addDeps, ",")
266266+ for dep := range deps {
268267 dep = strings.TrimSpace(dep)
269268 if dep != "" && !slices.Contains(task.DependsOn, dep) {
270269 task.DependsOn = append(task.DependsOn, dep)
···272271 }
273272 }
274273275275- // Handle dependency removals
276274 if removeDeps != "" {
277277- deps := strings.Split(removeDeps, ",")
278278- for _, dep := range deps {
275275+ deps := strings.SplitSeq(removeDeps, ",")
276276+ for dep := range deps {
279277 dep = strings.TrimSpace(dep)
280278 task.DependsOn = removeString(task.DependsOn, dep)
281279 }
···848846 return fmt.Errorf("failed to marshal task to JSON: %w", err)
849847 }
850848 fmt.Println(string(jsonData))
849849+ return nil
850850+}
851851+852852+// SetRecur sets the recurrence rule for a task
853853+func (h *TaskHandler) SetRecur(ctx context.Context, taskID, rule, until string) error {
854854+ var task *models.Task
855855+ var err error
856856+857857+ if id, err_ := strconv.ParseInt(taskID, 10, 64); err_ == nil {
858858+ task, err = h.repos.Tasks.Get(ctx, id)
859859+ } else {
860860+ task, err = h.repos.Tasks.GetByUUID(ctx, taskID)
861861+ }
862862+863863+ if err != nil {
864864+ return fmt.Errorf("failed to find task: %w", err)
865865+ }
866866+867867+ if rule != "" {
868868+ task.Recur = models.RRule(rule)
869869+ }
870870+871871+ if until != "" {
872872+ if untilTime, err := time.Parse("2006-01-02", until); err == nil {
873873+ task.Until = &untilTime
874874+ } else {
875875+ return fmt.Errorf("invalid until date format, use YYYY-MM-DD: %w", err)
876876+ }
877877+ }
878878+879879+ err = h.repos.Tasks.Update(ctx, task)
880880+ if err != nil {
881881+ return fmt.Errorf("failed to update task recurrence: %w", err)
882882+ }
883883+884884+ fmt.Printf("Recurrence set for task (ID: %d): %s\n", task.ID, task.Description)
885885+ if task.Recur != "" {
886886+ fmt.Printf("Rule: %s\n", task.Recur)
887887+ }
888888+ if task.Until != nil {
889889+ fmt.Printf("Until: %s\n", task.Until.Format("2006-01-02"))
890890+ }
891891+892892+ return nil
893893+}
894894+895895+// ClearRecur clears the recurrence rule from a task
896896+func (h *TaskHandler) ClearRecur(ctx context.Context, taskID string) error {
897897+ var task *models.Task
898898+ var err error
899899+900900+ if id, err_ := strconv.ParseInt(taskID, 10, 64); err_ == nil {
901901+ task, err = h.repos.Tasks.Get(ctx, id)
902902+ } else {
903903+ task, err = h.repos.Tasks.GetByUUID(ctx, taskID)
904904+ }
905905+906906+ if err != nil {
907907+ return fmt.Errorf("failed to find task: %w", err)
908908+ }
909909+910910+ task.Recur = ""
911911+ task.Until = nil
912912+913913+ err = h.repos.Tasks.Update(ctx, task)
914914+ if err != nil {
915915+ return fmt.Errorf("failed to clear task recurrence: %w", err)
916916+ }
917917+918918+ fmt.Printf("Recurrence cleared for task (ID: %d): %s\n", task.ID, task.Description)
919919+ return nil
920920+}
921921+922922+// ShowRecur displays the recurrence details for a task
923923+func (h *TaskHandler) ShowRecur(ctx context.Context, taskID string) error {
924924+ var task *models.Task
925925+ var err error
926926+927927+ if id, err_ := strconv.ParseInt(taskID, 10, 64); err_ == nil {
928928+ task, err = h.repos.Tasks.Get(ctx, id)
929929+ } else {
930930+ task, err = h.repos.Tasks.GetByUUID(ctx, taskID)
931931+ }
932932+933933+ if err != nil {
934934+ return fmt.Errorf("failed to find task: %w", err)
935935+ }
936936+937937+ fmt.Printf("Task (ID: %d): %s\n", task.ID, task.Description)
938938+ if task.Recur != "" {
939939+ fmt.Printf("Recurrence rule: %s\n", task.Recur)
940940+ if task.Until != nil {
941941+ fmt.Printf("Recurrence until: %s\n", task.Until.Format("2006-01-02"))
942942+ } else {
943943+ fmt.Printf("Recurrence until: (no end date)\n")
944944+ }
945945+ } else {
946946+ fmt.Printf("No recurrence set\n")
947947+ }
948948+949949+ return nil
950950+}
951951+952952+// AddDep adds a dependency to a task
953953+func (h *TaskHandler) AddDep(ctx context.Context, taskID, dependsOnUUID string) error {
954954+ var task *models.Task
955955+ var err error
956956+957957+ if id, err_ := strconv.ParseInt(taskID, 10, 64); err_ == nil {
958958+ task, err = h.repos.Tasks.Get(ctx, id)
959959+ } else {
960960+ task, err = h.repos.Tasks.GetByUUID(ctx, taskID)
961961+ }
962962+963963+ if err != nil {
964964+ return fmt.Errorf("failed to find task: %w", err)
965965+ }
966966+967967+ if _, err := h.repos.Tasks.GetByUUID(ctx, dependsOnUUID); err != nil {
968968+ return fmt.Errorf("dependency task not found: %w", err)
969969+ }
970970+971971+ err = h.repos.Tasks.AddDependency(ctx, task.UUID, dependsOnUUID)
972972+ if err != nil {
973973+ return fmt.Errorf("failed to add dependency: %w", err)
974974+ }
975975+976976+ fmt.Printf("Dependency added to task (ID: %d): %s\n", task.ID, task.Description)
977977+ fmt.Printf("Now depends on: %s\n", dependsOnUUID)
978978+979979+ return nil
980980+}
981981+982982+// RemoveDep removes a dependency from a task
983983+func (h *TaskHandler) RemoveDep(ctx context.Context, taskID, dependsOnUUID string) error {
984984+ var task *models.Task
985985+ var err error
986986+987987+ if id, err_ := strconv.ParseInt(taskID, 10, 64); err_ == nil {
988988+ task, err = h.repos.Tasks.Get(ctx, id)
989989+ } else {
990990+ task, err = h.repos.Tasks.GetByUUID(ctx, taskID)
991991+ }
992992+993993+ if err != nil {
994994+ return fmt.Errorf("failed to find task: %w", err)
995995+ }
996996+997997+ err = h.repos.Tasks.RemoveDependency(ctx, task.UUID, dependsOnUUID)
998998+ if err != nil {
999999+ return fmt.Errorf("failed to remove dependency: %w", err)
10001000+ }
10011001+10021002+ fmt.Printf("Dependency removed from task (ID: %d): %s\n", task.ID, task.Description)
10031003+ fmt.Printf("No longer depends on: %s\n", dependsOnUUID)
10041004+10051005+ return nil
10061006+}
10071007+10081008+// ListDeps lists all dependencies for a task
10091009+func (h *TaskHandler) ListDeps(ctx context.Context, taskID string) error {
10101010+ var task *models.Task
10111011+ var err error
10121012+10131013+ if id, err_ := strconv.ParseInt(taskID, 10, 64); err_ == nil {
10141014+ task, err = h.repos.Tasks.Get(ctx, id)
10151015+ } else {
10161016+ task, err = h.repos.Tasks.GetByUUID(ctx, taskID)
10171017+ }
10181018+10191019+ if err != nil {
10201020+ return fmt.Errorf("failed to find task: %w", err)
10211021+ }
10221022+10231023+ fmt.Printf("Task (ID: %d): %s\n", task.ID, task.Description)
10241024+10251025+ if len(task.DependsOn) == 0 {
10261026+ fmt.Printf("No dependencies\n")
10271027+ return nil
10281028+ }
10291029+10301030+ fmt.Printf("Depends on %d task(s):\n", len(task.DependsOn))
10311031+ for _, depUUID := range task.DependsOn {
10321032+ depTask, err := h.repos.Tasks.GetByUUID(ctx, depUUID)
10331033+ if err != nil {
10341034+ fmt.Printf(" - %s (not found)\n", depUUID)
10351035+ continue
10361036+ }
10371037+ fmt.Printf(" - [%d] %s (UUID: %s)\n", depTask.ID, depTask.Description, depTask.UUID)
10381038+ }
10391039+10401040+ return nil
10411041+}
10421042+10431043+// BlockedByDep shows tasks that are blocked by the given task
10441044+func (h *TaskHandler) BlockedByDep(ctx context.Context, taskID string) error {
10451045+ var task *models.Task
10461046+ var err error
10471047+10481048+ if id, err_ := strconv.ParseInt(taskID, 10, 64); err_ == nil {
10491049+ task, err = h.repos.Tasks.Get(ctx, id)
10501050+ } else {
10511051+ task, err = h.repos.Tasks.GetByUUID(ctx, taskID)
10521052+ }
10531053+10541054+ if err != nil {
10551055+ return fmt.Errorf("failed to find task: %w", err)
10561056+ }
10571057+10581058+ fmt.Printf("Task (ID: %d): %s\n", task.ID, task.Description)
10591059+10601060+ dependents, err := h.repos.Tasks.GetDependents(ctx, task.UUID)
10611061+ if err != nil {
10621062+ return fmt.Errorf("failed to get dependent tasks: %w", err)
10631063+ }
10641064+10651065+ if len(dependents) == 0 {
10661066+ fmt.Printf("No tasks are blocked by this task\n")
10671067+ return nil
10681068+ }
10691069+10701070+ fmt.Printf("Blocks %d task(s):\n", len(dependents))
10711071+ for _, dep := range dependents {
10721072+ fmt.Printf(" - [%d] %s\n", dep.ID, dep.Description)
10731073+ }
10741074+8511075 return nil
8521076}
8531077