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 )
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}