cli + tui to publish to leaflet (wip) & manage tasks, notes & watch/read lists 馃崈
charm leaflet readability golang
at main 756 lines 25 kB view raw
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}