cli + tui to publish to leaflet (wip) & manage tasks, notes & watch/read lists 馃崈
charm leaflet readability golang
29
fork

Configure Feed

Select the types of activity you want to include in your feed.

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