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