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