cli + tui to publish to leaflet (wip) & manage tasks, notes & watch/read lists 馃崈
charm
leaflet
readability
golang
1package main
2
3import (
4 "fmt"
5 "strconv"
6 "strings"
7
8 "github.com/spf13/cobra"
9 "github.com/stormlightlabs/noteleaf/internal/handlers"
10)
11
12// TaskCommand implements CommandGroup for task-related commands
13type TaskCommand struct {
14 handler *handlers.TaskHandler
15}
16
17// NewTaskCommand creates a new TaskCommands with the given handler
18func NewTaskCommand(handler *handlers.TaskHandler) *TaskCommand {
19 return &TaskCommand{handler: handler}
20}
21
22func (c *TaskCommand) Create() *cobra.Command {
23 root := &cobra.Command{
24 Use: "todo",
25 Aliases: []string{"task"},
26 Short: "task management",
27 Long: `Manage tasks with TaskWarrior-inspired features.
28
29Track todos with priorities, projects, contexts, and tags. Supports hierarchical
30tasks with parent/child relationships, task dependencies, recurring tasks, and
31time tracking. Tasks can be filtered by status, priority, project, or context.`,
32 }
33
34 root.AddGroup(
35 &cobra.Group{ID: "task-ops", Title: "Basic Operations"},
36 &cobra.Group{ID: "task-meta", Title: "Metadata"},
37 &cobra.Group{ID: "task-tracking", Title: "Tracking"},
38 &cobra.Group{ID: "task-reports", Title: "Reports & Views"},
39 )
40
41 for _, init := range []func(*handlers.TaskHandler) *cobra.Command{
42 addTaskCmd, listTaskCmd, viewTaskCmd, updateTaskCmd, editTaskCmd, deleteTaskCmd, taskAnnotateCmd, taskBulkEditCmd,
43 } {
44 cmd := init(c.handler)
45 cmd.GroupID = "task-ops"
46 root.AddCommand(cmd)
47 }
48
49 for _, init := range []func(*handlers.TaskHandler) *cobra.Command{
50 taskProjectsCmd, taskTagsCmd, taskContextsCmd,
51 } {
52 cmd := init(c.handler)
53 cmd.GroupID = "task-meta"
54 root.AddCommand(cmd)
55 }
56
57 for _, init := range []func(*handlers.TaskHandler) *cobra.Command{
58 timesheetViewCmd, taskStartCmd, taskStopCmd, taskCompleteCmd, taskRecurCmd, taskDependCmd, taskUndoCmd, taskHistoryCmd,
59 } {
60 cmd := init(c.handler)
61 cmd.GroupID = "task-tracking"
62 root.AddCommand(cmd)
63 }
64
65 for _, init := range []func(*handlers.TaskHandler) *cobra.Command{
66 nextActionsCmd, reportCompletedCmd, reportWaitingCmd, reportBlockedCmd, calendarCmd,
67 } {
68 cmd := init(c.handler)
69 cmd.GroupID = "task-reports"
70 root.AddCommand(cmd)
71 }
72
73 return root
74}
75
76func addTaskCmd(h *handlers.TaskHandler) *cobra.Command {
77 cmd := &cobra.Command{
78 Use: "add [description]",
79 Short: "Add a new task",
80 Aliases: []string{"create", "new"},
81 Long: `Create a new task with description and optional attributes.
82
83Tasks can be created with priority levels (low, medium, high, urgent), assigned
84to projects and contexts, tagged for organization, and configured with due dates
85and recurrence rules. Dependencies can be established to ensure tasks are
86completed in order.
87
88Examples:
89 noteleaf todo add "Write documentation" --priority high --project docs
90 noteleaf todo add "Weekly review" --recur "FREQ=WEEKLY" --due 2024-01-15`,
91 Args: cobra.MinimumNArgs(1),
92 RunE: func(c *cobra.Command, args []string) error {
93 description := strings.Join(args, " ")
94 priority, _ := c.Flags().GetString("priority")
95 project, _ := c.Flags().GetString("project")
96 context, _ := c.Flags().GetString("context")
97 due, _ := c.Flags().GetString("due")
98 wait, _ := c.Flags().GetString("wait")
99 scheduled, _ := c.Flags().GetString("scheduled")
100 recur, _ := c.Flags().GetString("recur")
101 until, _ := c.Flags().GetString("until")
102 parent, _ := c.Flags().GetString("parent")
103 dependsOn, _ := c.Flags().GetString("depends-on")
104 tags, _ := c.Flags().GetStringSlice("tags")
105
106 defer h.Close()
107 // TODO: Make a CreateTask struct
108 return h.Create(c.Context(), description, priority, project, context, due, wait, scheduled, recur, until, parent, dependsOn, tags)
109 },
110 }
111 addCommonTaskFlags(cmd)
112 addDueDateFlag(cmd)
113 addWaitScheduledFlags(cmd)
114 addRecurrenceFlags(cmd)
115 addParentFlag(cmd)
116 addDependencyFlags(cmd)
117
118 return cmd
119}
120
121func listTaskCmd(h *handlers.TaskHandler) *cobra.Command {
122 cmd := &cobra.Command{
123 Use: "list",
124 Short: "List tasks",
125 Aliases: []string{"ls"},
126 Long: `List tasks with optional filtering and display modes.
127
128By default, shows tasks in an interactive TaskWarrior-like interface.
129Use --static to show a simple text list instead.
130Use --all to show all tasks, otherwise only pending tasks are shown.`,
131 RunE: func(c *cobra.Command, args []string) error {
132 static, _ := c.Flags().GetBool("static")
133 showAll, _ := c.Flags().GetBool("all")
134 status, _ := c.Flags().GetString("status")
135 priority, _ := c.Flags().GetString("priority")
136 project, _ := c.Flags().GetString("project")
137 context, _ := c.Flags().GetString("context")
138 sortBy, _ := c.Flags().GetString("sort")
139
140 defer h.Close()
141 // TODO: TaskFilter struct
142 return h.List(c.Context(), static, showAll, status, priority, project, context, sortBy)
143 },
144 }
145 cmd.Flags().BoolP("interactive", "i", false, "Force interactive mode (default)")
146 cmd.Flags().Bool("static", false, "Use static text output instead of interactive")
147 cmd.Flags().BoolP("all", "a", false, "Show all tasks (default: pending only)")
148 cmd.Flags().String("status", "", "Filter by status")
149 cmd.Flags().String("priority", "", "Filter by priority")
150 cmd.Flags().String("project", "", "Filter by project")
151 cmd.Flags().String("context", "", "Filter by context")
152 cmd.Flags().String("sort", "", "Sort by (urgency)")
153
154 return cmd
155}
156
157func viewTaskCmd(handler *handlers.TaskHandler) *cobra.Command {
158 viewCmd := &cobra.Command{
159 Use: "view [task-id]",
160 Short: "View task by ID",
161 Long: `Display detailed information for a specific task.
162
163Shows all task attributes including description, status, priority, project,
164context, tags, due date, creation time, and modification history. Use --json
165for machine-readable output or --no-metadata to show only the description.`,
166 Args: cobra.ExactArgs(1),
167 RunE: func(cmd *cobra.Command, args []string) error {
168 format, _ := cmd.Flags().GetString("format")
169 jsonOutput, _ := cmd.Flags().GetBool("json")
170 noMetadata, _ := cmd.Flags().GetBool("no-metadata")
171
172 defer handler.Close()
173 return handler.View(cmd.Context(), args, format, jsonOutput, noMetadata)
174 },
175 }
176 addOutputFlags(viewCmd)
177
178 return viewCmd
179}
180
181func updateTaskCmd(handler *handlers.TaskHandler) *cobra.Command {
182 updateCmd := &cobra.Command{
183 Use: "update [task-id]",
184 Short: "Update task properties",
185 Long: `Modify attributes of an existing task.
186
187Update any task property including description, status, priority, project,
188context, due date, recurrence rule, or parent task. Add or remove tags and
189dependencies. Multiple attributes can be updated in a single command.
190
191Examples:
192 noteleaf todo update 123 --priority urgent --due tomorrow
193 noteleaf todo update 456 --add-tag urgent --project website`,
194 Args: cobra.ExactArgs(1),
195 RunE: func(cmd *cobra.Command, args []string) error {
196 taskID := args[0]
197 description, _ := cmd.Flags().GetString("description")
198 status, _ := cmd.Flags().GetString("status")
199 priority, _ := cmd.Flags().GetString("priority")
200 project, _ := cmd.Flags().GetString("project")
201 context, _ := cmd.Flags().GetString("context")
202 due, _ := cmd.Flags().GetString("due")
203 recur, _ := cmd.Flags().GetString("recur")
204 until, _ := cmd.Flags().GetString("until")
205 parent, _ := cmd.Flags().GetString("parent")
206 addTags, _ := cmd.Flags().GetStringSlice("add-tag")
207 removeTags, _ := cmd.Flags().GetStringSlice("remove-tag")
208 addDeps, _ := cmd.Flags().GetString("add-depends")
209 removeDeps, _ := cmd.Flags().GetString("remove-depends")
210
211 defer handler.Close()
212 return handler.Update(cmd.Context(), taskID, description, status, priority, project, context, due, recur, until, parent, addTags, removeTags, addDeps, removeDeps)
213 },
214 }
215 updateCmd.Flags().String("description", "", "Update task description")
216 updateCmd.Flags().String("status", "", "Update task status")
217 addCommonTaskFlags(updateCmd)
218 addDueDateFlag(updateCmd)
219 addRecurrenceFlags(updateCmd)
220 addParentFlag(updateCmd)
221 updateCmd.Flags().StringSlice("add-tag", []string{}, "Add tags to task")
222 updateCmd.Flags().StringSlice("remove-tag", []string{}, "Remove tags from task")
223 updateCmd.Flags().String("add-depends", "", "Add task dependencies (comma-separated UUIDs)")
224 updateCmd.Flags().String("remove-depends", "", "Remove task dependencies (comma-separated UUIDs)")
225
226 return updateCmd
227}
228
229func taskProjectsCmd(h *handlers.TaskHandler) *cobra.Command {
230 cmd := &cobra.Command{
231 Use: "projects",
232 Short: "List projects",
233 Aliases: []string{"proj"},
234 Long: `Display all projects with task counts.
235
236Shows each project used in your tasks along with the number of tasks in each
237project. Use --todo-txt to format output with +project syntax for compatibility
238with todo.txt tools.`,
239 RunE: func(c *cobra.Command, args []string) error {
240 static, _ := c.Flags().GetBool("static")
241 todoTxt, _ := c.Flags().GetBool("todo-txt")
242
243 defer h.Close()
244 return h.ListProjects(c.Context(), static, todoTxt)
245 },
246 }
247 cmd.Flags().Bool("static", false, "Use static text output instead of interactive")
248 cmd.Flags().Bool("todo-txt", false, "Format output with +project prefix for todo.txt compatibility")
249
250 return cmd
251}
252
253func taskTagsCmd(h *handlers.TaskHandler) *cobra.Command {
254 cmd := &cobra.Command{
255 Use: "tags",
256 Short: "List tags",
257 Aliases: []string{"t"},
258 Long: `Display all tags used across tasks.
259
260Shows each tag with the number of tasks using it. Tags provide flexible
261categorization orthogonal to projects and contexts.`,
262 RunE: func(c *cobra.Command, args []string) error {
263 static, _ := c.Flags().GetBool("static")
264 defer h.Close()
265 return h.ListTags(c.Context(), static)
266 },
267 }
268 cmd.Flags().Bool("static", false, "Use static text output instead of interactive")
269 return cmd
270}
271
272func taskStartCmd(h *handlers.TaskHandler) *cobra.Command {
273 cmd := &cobra.Command{
274 Use: "start [task-id]",
275 Short: "Start time tracking for a task",
276 Long: `Begin tracking time spent on a task.
277
278Records the start time for a work session. Only one task can be actively
279tracked at a time. Use --note to add a description of what you're working on.`,
280 Args: cobra.ExactArgs(1),
281 RunE: func(c *cobra.Command, args []string) error {
282 taskID := args[0]
283 description, _ := c.Flags().GetString("note")
284
285 defer h.Close()
286 return h.Start(c.Context(), taskID, description)
287 },
288 }
289 cmd.Flags().StringP("note", "n", "", "Add a note to the time entry")
290 return cmd
291}
292
293func taskStopCmd(h *handlers.TaskHandler) *cobra.Command {
294 return &cobra.Command{
295 Use: "stop [task-id]",
296 Short: "Stop time tracking for a task",
297 Long: `End time tracking for the active task.
298
299Records the end time and calculates duration for the current work session.
300Duration is added to the task's total time tracked.`,
301 Args: cobra.ExactArgs(1),
302 RunE: func(c *cobra.Command, args []string) error {
303 taskID := args[0]
304 defer h.Close()
305 return h.Stop(c.Context(), taskID)
306 },
307 }
308}
309
310func timesheetViewCmd(h *handlers.TaskHandler) *cobra.Command {
311 cmd := &cobra.Command{
312 Use: "timesheet",
313 Short: "Show time tracking summary",
314 Long: `Show time tracking summary for tasks.
315
316By default shows time entries for the last 7 days.
317Use --task to show timesheet for a specific task.
318Use --days to change the date range.`,
319 RunE: func(c *cobra.Command, args []string) error {
320 days, _ := c.Flags().GetInt("days")
321 taskID, _ := c.Flags().GetString("task")
322
323 defer h.Close()
324 return h.Timesheet(c.Context(), days, taskID)
325 },
326 }
327 cmd.Flags().IntP("days", "d", 7, "Number of days to show in timesheet")
328 cmd.Flags().StringP("task", "t", "", "Show timesheet for specific task ID")
329 return cmd
330}
331
332func editTaskCmd(h *handlers.TaskHandler) *cobra.Command {
333 return &cobra.Command{
334 Use: "edit [task-id]",
335 Short: "Edit task interactively with status picker and priority toggle",
336 Aliases: []string{"e"},
337 Long: `Open interactive editor for task modification.
338
339Provides a user-friendly interface with status picker and priority toggle.
340Easier than using multiple command-line flags for complex updates.`,
341 Args: cobra.ExactArgs(1),
342 RunE: func(c *cobra.Command, args []string) error {
343 taskID := args[0]
344 defer h.Close()
345 return h.EditInteractive(c.Context(), taskID)
346 },
347 }
348}
349
350func deleteTaskCmd(h *handlers.TaskHandler) *cobra.Command {
351 return &cobra.Command{
352 Use: "delete [task-id]",
353 Short: "Delete a task",
354 Long: `Permanently remove a task from the database.
355
356This operation cannot be undone. Consider updating the task status to
357'deleted' instead if you want to preserve the record for historical purposes.`,
358 Args: cobra.ExactArgs(1),
359 RunE: func(c *cobra.Command, args []string) error {
360 defer h.Close()
361 return h.Delete(c.Context(), args)
362 },
363 }
364}
365
366func taskContextsCmd(h *handlers.TaskHandler) *cobra.Command {
367 cmd := &cobra.Command{
368 Use: "contexts",
369 Short: "List contexts (locations)",
370 Aliases: []string{"con", "loc", "ctx", "locations"},
371 Long: `Display all contexts with task counts.
372
373Contexts represent locations or environments where tasks can be completed (e.g.,
374@home, @office, @errands). Use --todo-txt to format output with @context syntax
375for compatibility with todo.txt tools.`,
376 RunE: func(c *cobra.Command, args []string) error {
377 static, _ := c.Flags().GetBool("static")
378 todoTxt, _ := c.Flags().GetBool("todo-txt")
379
380 defer h.Close()
381 return h.ListContexts(c.Context(), static, todoTxt)
382 },
383 }
384 cmd.Flags().Bool("static", false, "Use static text output instead of interactive")
385 cmd.Flags().Bool("todo-txt", false, "Format output with @context prefix for todo.txt compatibility")
386 return cmd
387}
388
389func taskCompleteCmd(h *handlers.TaskHandler) *cobra.Command {
390 return &cobra.Command{
391 Use: "done [task-id]",
392 Short: "Mark task as completed",
393 Aliases: []string{"complete"},
394 Long: `Mark a task as completed with current timestamp.
395
396Sets the task status to 'completed' and records the completion time. For
397recurring tasks, generates the next instance based on the recurrence rule.`,
398 Args: cobra.ExactArgs(1),
399 RunE: func(c *cobra.Command, args []string) error {
400 defer h.Close()
401 return h.Done(c.Context(), args)
402 },
403 }
404}
405
406func taskRecurCmd(h *handlers.TaskHandler) *cobra.Command {
407 root := &cobra.Command{
408 Use: "recur",
409 Short: "Manage task recurrence",
410 Aliases: []string{"repeat"},
411 Long: `Configure recurring task patterns.
412
413Create tasks that repeat on a schedule using iCalendar recurrence rules (RRULE).
414Supports daily, weekly, monthly, and yearly patterns with optional end dates.`,
415 }
416
417 setCmd := &cobra.Command{
418 Use: "set [task-id]",
419 Short: "Set recurrence rule for a task",
420 Long: `Apply a recurrence rule to create repeating task instances.
421
422Uses iCalendar RRULE syntax (e.g., "FREQ=DAILY" for daily tasks, "FREQ=WEEKLY;BYDAY=MO,WE,FR"
423for specific weekdays). When a recurring task is completed, the next instance is
424automatically generated.
425
426Examples:
427 noteleaf todo recur set 123 --rule "FREQ=DAILY"
428 noteleaf todo recur set 456 --rule "FREQ=WEEKLY;BYDAY=MO" --until 2024-12-31`,
429 Args: cobra.ExactArgs(1),
430 RunE: func(c *cobra.Command, args []string) error {
431 rule, _ := c.Flags().GetString("rule")
432 until, _ := c.Flags().GetString("until")
433 defer h.Close()
434 return h.SetRecur(c.Context(), args[0], rule, until)
435 },
436 }
437 setCmd.Flags().String("rule", "", "Recurrence rule (e.g., FREQ=DAILY)")
438 setCmd.Flags().String("until", "", "Recurrence end date (YYYY-MM-DD)")
439
440 clearCmd := &cobra.Command{
441 Use: "clear [task-id]",
442 Short: "Clear recurrence rule from a task",
443 Long: `Remove recurrence from a task.
444
445Converts a recurring task to a one-time task. Existing future instances are not
446affected.`,
447 Args: cobra.ExactArgs(1),
448 RunE: func(c *cobra.Command, args []string) error {
449 defer h.Close()
450 return h.ClearRecur(c.Context(), args[0])
451 },
452 }
453
454 showCmd := &cobra.Command{
455 Use: "show [task-id]",
456 Short: "Show recurrence details for a task",
457 Long: `Display recurrence rule and schedule information.
458
459Shows the RRULE pattern, next occurrence date, and recurrence end date if
460configured.`,
461 Args: cobra.ExactArgs(1),
462 RunE: func(c *cobra.Command, args []string) error {
463 defer h.Close()
464 return h.ShowRecur(c.Context(), args[0])
465 },
466 }
467
468 root.AddCommand(setCmd, clearCmd, showCmd)
469 return root
470}
471
472func nextActionsCmd(h *handlers.TaskHandler) *cobra.Command {
473 cmd := &cobra.Command{
474 Use: "next",
475 Short: "Show next actions (actionable tasks sorted by urgency)",
476 Aliases: []string{"na"},
477 Long: `Display actionable tasks sorted by urgency score.
478
479Shows tasks that can be worked on now (not waiting, not blocked, not completed),
480ordered by their computed urgency based on priority, due date, age, and other factors.`,
481 RunE: func(c *cobra.Command, args []string) error {
482 limit, _ := c.Flags().GetInt("limit")
483 defer h.Close()
484 return h.NextActions(c.Context(), limit)
485 },
486 }
487 cmd.Flags().IntP("limit", "n", 10, "Limit number of tasks shown")
488 return cmd
489}
490
491func reportCompletedCmd(h *handlers.TaskHandler) *cobra.Command {
492 cmd := &cobra.Command{
493 Use: "completed",
494 Short: "Show completed tasks",
495 Long: "Display tasks that have been completed, sorted by completion date.",
496 RunE: func(c *cobra.Command, args []string) error {
497 limit, _ := c.Flags().GetInt("limit")
498 defer h.Close()
499 return h.ReportCompleted(c.Context(), limit)
500 },
501 }
502 cmd.Flags().IntP("limit", "n", 20, "Limit number of tasks shown")
503 return cmd
504}
505
506func reportWaitingCmd(h *handlers.TaskHandler) *cobra.Command {
507 cmd := &cobra.Command{
508 Use: "waiting",
509 Short: "Show waiting tasks",
510 Long: "Display tasks that are waiting for a specific date before becoming actionable.",
511 RunE: func(c *cobra.Command, args []string) error {
512 defer h.Close()
513 return h.ReportWaiting(c.Context())
514 },
515 }
516 return cmd
517}
518
519func reportBlockedCmd(h *handlers.TaskHandler) *cobra.Command {
520 cmd := &cobra.Command{
521 Use: "blocked",
522 Short: "Show blocked tasks",
523 Long: "Display tasks that are blocked by dependencies on other tasks.",
524 RunE: func(c *cobra.Command, args []string) error {
525 defer h.Close()
526 return h.ReportBlocked(c.Context())
527 },
528 }
529 return cmd
530}
531
532func calendarCmd(h *handlers.TaskHandler) *cobra.Command {
533 cmd := &cobra.Command{
534 Use: "calendar",
535 Short: "Show tasks in calendar view",
536 Aliases: []string{"cal"},
537 Long: `Display tasks with due dates in a calendar format.
538
539Shows tasks organized by week and day, making it easy to see upcoming deadlines
540and plan your work schedule.`,
541 RunE: func(c *cobra.Command, args []string) error {
542 weeks, _ := c.Flags().GetInt("weeks")
543 defer h.Close()
544 return h.Calendar(c.Context(), weeks)
545 },
546 }
547 cmd.Flags().IntP("weeks", "w", 4, "Number of weeks to show")
548 return cmd
549}
550
551func taskDependCmd(h *handlers.TaskHandler) *cobra.Command {
552 root := &cobra.Command{
553 Use: "depend",
554 Short: "Manage task dependencies",
555 Aliases: []string{"dep", "deps"},
556 Long: `Create and manage task dependencies.
557
558Establish relationships where one task must be completed before another can
559begin. Useful for multi-step workflows and project management.`,
560 }
561
562 addCmd := &cobra.Command{
563 Use: "add [task-id] [depends-on-uuid]",
564 Short: "Add a dependency to a task",
565 Long: `Make a task dependent on another task's completion.
566
567The first task cannot be started until the second task is completed. Use task
568UUIDs to specify dependencies.`,
569 Args: cobra.ExactArgs(2),
570 RunE: func(c *cobra.Command, args []string) error {
571 defer h.Close()
572 return h.AddDep(c.Context(), args[0], args[1])
573 },
574 }
575
576 removeCmd := &cobra.Command{
577 Use: "remove [task-id] [depends-on-uuid]",
578 Short: "Remove a dependency from a task",
579 Aliases: []string{"rm"},
580 Long: "Delete a dependency relationship between two tasks.",
581 Args: cobra.ExactArgs(2),
582 RunE: func(c *cobra.Command, args []string) error {
583 defer h.Close()
584 return h.RemoveDep(c.Context(), args[0], args[1])
585 },
586 }
587
588 listCmd := &cobra.Command{
589 Use: "list [task-id]",
590 Short: "List dependencies for a task",
591 Aliases: []string{"ls"},
592 Long: "Show all tasks that must be completed before this task can be started.",
593 Args: cobra.ExactArgs(1),
594 RunE: func(c *cobra.Command, args []string) error {
595 defer h.Close()
596 return h.ListDeps(c.Context(), args[0])
597 },
598 }
599
600 blockedByCmd := &cobra.Command{
601 Use: "blocked-by [task-id]",
602 Short: "Show tasks blocked by this task",
603 Long: "Display all tasks that depend on this task's completion.",
604 Args: cobra.ExactArgs(1),
605 RunE: func(c *cobra.Command, args []string) error {
606 defer h.Close()
607 return h.BlockedByDep(c.Context(), args[0])
608 },
609 }
610
611 root.AddCommand(addCmd, removeCmd, listCmd, blockedByCmd)
612 return root
613}
614
615func taskAnnotateCmd(h *handlers.TaskHandler) *cobra.Command {
616 root := &cobra.Command{
617 Use: "annotate",
618 Aliases: []string{"note"},
619 Short: "Manage task annotations",
620 Long: `Add, list, or remove annotations on tasks.
621
622Annotations are timestamped notes that provide context and updates
623about a task's progress or relevant information.`,
624 }
625
626 addCmd := &cobra.Command{
627 Use: "add <task-id> <annotation>",
628 Short: "Add an annotation to a task",
629 Aliases: []string{"create"},
630 Args: cobra.MinimumNArgs(2),
631 RunE: func(c *cobra.Command, args []string) error {
632 taskID := args[0]
633 annotation := strings.Join(args[1:], " ")
634 defer h.Close()
635 return h.Annotate(c.Context(), taskID, annotation)
636 },
637 }
638
639 listCmd := &cobra.Command{
640 Use: "list <task-id>",
641 Short: "List all annotations for a task",
642 Aliases: []string{"ls", "show"},
643 Args: cobra.ExactArgs(1),
644 RunE: func(c *cobra.Command, args []string) error {
645 defer h.Close()
646 return h.ListAnnotations(c.Context(), args[0])
647 },
648 }
649
650 removeCmd := &cobra.Command{
651 Use: "remove <task-id> <index>",
652 Short: "Remove an annotation by index",
653 Aliases: []string{"rm", "delete"},
654 Args: cobra.ExactArgs(2),
655 RunE: func(c *cobra.Command, args []string) error {
656 taskID := args[0]
657 index, err := strconv.Atoi(args[1])
658 if err != nil {
659 return fmt.Errorf("invalid annotation index: %w", err)
660 }
661 defer h.Close()
662 return h.RemoveAnnotation(c.Context(), taskID, index)
663 },
664 }
665
666 root.AddCommand(addCmd, listCmd, removeCmd)
667 return root
668}
669
670func taskBulkEditCmd(h *handlers.TaskHandler) *cobra.Command {
671 cmd := &cobra.Command{
672 Use: "bulk-edit <task-id>...",
673 Aliases: []string{"bulk"},
674 Short: "Update multiple tasks at once",
675 Long: `Update multiple tasks with the same changes.
676
677Allows batch updates to status, priority, project, context, and tags.
678Use --add-tags to add tags without replacing existing ones.
679Use --remove-tags to remove specific tags from tasks.
680
681Examples:
682 noteleaf todo bulk-edit 1 2 3 --status done
683 noteleaf todo bulk-edit 1 2 --project web --priority high
684 noteleaf todo bulk-edit 1 2 3 --add-tags urgent,review`,
685 Args: cobra.MinimumNArgs(1),
686 RunE: func(c *cobra.Command, args []string) error {
687 status, _ := c.Flags().GetString("status")
688 priority, _ := c.Flags().GetString("priority")
689 project, _ := c.Flags().GetString("project")
690 context, _ := c.Flags().GetString("context")
691 tags, _ := c.Flags().GetStringSlice("tags")
692 addTags, _ := c.Flags().GetBool("add-tags")
693 removeTags, _ := c.Flags().GetBool("remove-tags")
694
695 defer h.Close()
696 return h.BulkEdit(c.Context(), args, status, priority, project, context, tags, addTags, removeTags)
697 },
698 }
699
700 cmd.Flags().String("status", "", "Set status for all tasks")
701 cmd.Flags().String("priority", "", "Set priority for all tasks")
702 cmd.Flags().String("project", "", "Set project for all tasks")
703 cmd.Flags().String("context", "", "Set context for all tasks")
704 cmd.Flags().StringSlice("tags", []string{}, "Set tags for all tasks")
705 cmd.Flags().Bool("add-tags", false, "Add tags instead of replacing")
706 cmd.Flags().Bool("remove-tags", false, "Remove specified tags")
707
708 return cmd
709}
710
711func taskUndoCmd(h *handlers.TaskHandler) *cobra.Command {
712 cmd := &cobra.Command{
713 Use: "undo <task-id>",
714 Short: "Undo the last change to a task",
715 Long: `Revert a task to its previous state before the last update.
716
717This command uses the task history to restore the task to how it was
718before the most recent modification.
719
720Examples:
721 noteleaf todo undo 1
722 noteleaf todo undo abc-123-uuid`,
723 Args: cobra.ExactArgs(1),
724 RunE: func(c *cobra.Command, args []string) error {
725 defer h.Close()
726 return h.UndoTask(c.Context(), args[0])
727 },
728 }
729
730 return cmd
731}
732
733func taskHistoryCmd(h *handlers.TaskHandler) *cobra.Command {
734 cmd := &cobra.Command{
735 Use: "history <task-id>",
736 Aliases: []string{"log"},
737 Short: "Show change history for a task",
738 Long: `Display the history of changes made to a task.
739
740Shows a chronological list of modifications with timestamps.
741
742Examples:
743 noteleaf todo history 1
744 noteleaf todo history 1 --limit 5`,
745 Args: cobra.ExactArgs(1),
746 RunE: func(c *cobra.Command, args []string) error {
747 limit, _ := c.Flags().GetInt("limit")
748 defer h.Close()
749 return h.ShowHistory(c.Context(), args[0], limit)
750 },
751 }
752
753 cmd.Flags().IntP("limit", "n", 10, "Limit number of history entries")
754
755 return cmd
756}