cli + tui to publish to leaflet (wip) & manage tasks, notes & watch/read lists ๐Ÿƒ
charm leaflet readability golang

refactor: use single task handler instance in task command

+348 -224
+81 -21
cmd/commands.go
··· 1 package main 2 3 import ( ··· 44 Short: "task management", 45 } 46 47 - root.AddCommand(&cobra.Command{ 48 Use: "add [description]", 49 Short: "Add a new task", 50 Aliases: []string{"create", "new"}, 51 Args: cobra.MinimumNArgs(1), 52 RunE: func(cmd *cobra.Command, args []string) error { 53 - return handlers.CreateTask(cmd.Context(), args) 54 }, 55 - }) 56 57 listCmd := &cobra.Command{ 58 Use: "list", ··· 70 priority, _ := cmd.Flags().GetString("priority") 71 project, _ := cmd.Flags().GetString("project") 72 73 - return handlers.ListTasks(cmd.Context(), static, showAll, status, priority, project) 74 }, 75 } 76 listCmd.Flags().BoolP("interactive", "i", false, "Force interactive mode (default)") ··· 81 listCmd.Flags().String("project", "", "Filter by project") 82 root.AddCommand(listCmd) 83 84 - root.AddCommand(&cobra.Command{ 85 Use: "view [task-id]", 86 Short: "View task by ID", 87 Args: cobra.ExactArgs(1), 88 RunE: func(cmd *cobra.Command, args []string) error { 89 - return handlers.ViewTask(cmd.Context(), args) 90 }, 91 - }) 92 93 root.AddCommand(&cobra.Command{ 94 Use: "update [task-id] [options...]", 95 Short: "Update task properties", 96 Args: cobra.MinimumNArgs(1), 97 RunE: func(cmd *cobra.Command, args []string) error { 98 - return handlers.UpdateTask(cmd.Context(), args) 99 }, 100 }) 101 ··· 104 Short: "Delete a task", 105 Args: cobra.ExactArgs(1), 106 RunE: func(cmd *cobra.Command, args []string) error { 107 - return handlers.DeleteTask(cmd.Context(), args) 108 }, 109 }) 110 ··· 114 Aliases: []string{"proj"}, 115 RunE: func(cmd *cobra.Command, args []string) error { 116 static, _ := cmd.Flags().GetBool("static") 117 - return handlers.ListProjects(cmd.Context(), static) 118 }, 119 } 120 projectsCmd.Flags().Bool("static", false, "Use static text output instead of interactive") ··· 126 Aliases: []string{"t"}, 127 RunE: func(cmd *cobra.Command, args []string) error { 128 static, _ := cmd.Flags().GetBool("static") 129 - return handlers.ListTags(cmd.Context(), static) 130 }, 131 } 132 tagsCmd.Flags().Bool("static", false, "Use static text output instead of interactive") ··· 148 Aliases: []string{"complete"}, 149 Args: cobra.ExactArgs(1), 150 RunE: func(cmd *cobra.Command, args []string) error { 151 - return handlers.DoneTask(cmd.Context(), args) 152 }, 153 }) 154 ··· 181 RunE: func(cmd *cobra.Command, args []string) error { 182 title := args[0] 183 fmt.Printf("Adding movie: %s\n", title) 184 - // TODO: Implement movie addition 185 return nil 186 }, 187 }) ··· 191 Short: "List movies in queue", 192 RunE: func(cmd *cobra.Command, args []string) error { 193 fmt.Println("Listing movies...") 194 - // TODO: Implement movie listing 195 return nil 196 }, 197 }) ··· 203 Args: cobra.ExactArgs(1), 204 RunE: func(cmd *cobra.Command, args []string) error { 205 fmt.Printf("Marking movie %s as watched\n", args[0]) 206 - // TODO: Implement movie watched status 207 return nil 208 }, 209 }) ··· 215 Args: cobra.ExactArgs(1), 216 RunE: func(cmd *cobra.Command, args []string) error { 217 fmt.Printf("Removing movie %s from queue\n", args[0]) 218 - // TODO: Implement movie removal 219 return nil 220 }, 221 }) ··· 236 RunE: func(cmd *cobra.Command, args []string) error { 237 title := args[0] 238 fmt.Printf("Adding TV show: %s\n", title) 239 - // TODO: Implement TV show addition 240 return nil 241 }, 242 }) ··· 246 Short: "List TV shows in queue", 247 RunE: func(cmd *cobra.Command, args []string) error { 248 fmt.Println("Listing TV shows...") 249 - // TODO: Implement TV show listing 250 return nil 251 }, 252 }) ··· 258 Args: cobra.ExactArgs(1), 259 RunE: func(cmd *cobra.Command, args []string) error { 260 fmt.Printf("Marking TV show %s as watched\n", args[0]) 261 - // TODO: Implement TV show watched status 262 return nil 263 }, 264 }) ··· 270 Args: cobra.ExactArgs(1), 271 RunE: func(cmd *cobra.Command, args []string) error { 272 fmt.Printf("Removing TV show %s from queue\n", args[0]) 273 - // TODO: Implement TV show removal 274 return nil 275 }, 276 }) ··· 546 RunE: func(cmd *cobra.Command, args []string) error { 547 key, value := args[0], args[1] 548 fmt.Printf("Setting config %s = %s\n", key, value) 549 - // TODO: Implement config management 550 return nil 551 }, 552 }
··· 1 + /* 2 + TODO: Implement movie addition 3 + TODO: Implement movie listing 4 + TODO: Implement movie watched status 5 + TODO: Implement movie removal 6 + TODO: Implement TV show addition 7 + TODO: Implement TV show listing 8 + TODO: Implement TV show watched status 9 + TODO: Implement TV show removal 10 + TODO: Implement config management 11 + */ 12 package main 13 14 import ( ··· 55 Short: "task management", 56 } 57 58 + addCmd := &cobra.Command{ 59 Use: "add [description]", 60 Short: "Add a new task", 61 Aliases: []string{"create", "new"}, 62 Args: cobra.MinimumNArgs(1), 63 RunE: func(cmd *cobra.Command, args []string) error { 64 + priority, _ := cmd.Flags().GetString("priority") 65 + project, _ := cmd.Flags().GetString("project") 66 + due, _ := cmd.Flags().GetString("due") 67 + tags, _ := cmd.Flags().GetStringSlice("tags") 68 + 69 + handler, err := handlers.NewTaskHandler() 70 + if err != nil { 71 + return err 72 + } 73 + defer handler.Close() 74 + return handler.Create(cmd.Context(), args, priority, project, due, tags) 75 }, 76 + } 77 + addCmd.Flags().StringP("priority", "p", "", "Set task priority") 78 + addCmd.Flags().String("project", "", "Set task project") 79 + addCmd.Flags().StringP("due", "d", "", "Set due date (YYYY-MM-DD)") 80 + addCmd.Flags().StringSliceP("tags", "t", []string{}, "Add tags to task") 81 + root.AddCommand(addCmd) 82 83 listCmd := &cobra.Command{ 84 Use: "list", ··· 96 priority, _ := cmd.Flags().GetString("priority") 97 project, _ := cmd.Flags().GetString("project") 98 99 + handler, err := handlers.NewTaskHandler() 100 + if err != nil { 101 + return err 102 + } 103 + defer handler.Close() 104 + return handler.List(cmd.Context(), static, showAll, status, priority, project) 105 }, 106 } 107 listCmd.Flags().BoolP("interactive", "i", false, "Force interactive mode (default)") ··· 112 listCmd.Flags().String("project", "", "Filter by project") 113 root.AddCommand(listCmd) 114 115 + viewCmd := &cobra.Command{ 116 Use: "view [task-id]", 117 Short: "View task by ID", 118 Args: cobra.ExactArgs(1), 119 RunE: func(cmd *cobra.Command, args []string) error { 120 + format, _ := cmd.Flags().GetString("format") 121 + jsonOutput, _ := cmd.Flags().GetBool("json") 122 + noMetadata, _ := cmd.Flags().GetBool("no-metadata") 123 + 124 + handler, err := handlers.NewTaskHandler() 125 + if err != nil { 126 + return err 127 + } 128 + defer handler.Close() 129 + return handler.View(cmd.Context(), args, format, jsonOutput, noMetadata) 130 }, 131 + } 132 + viewCmd.Flags().String("format", "detailed", "Output format (detailed, brief)") 133 + viewCmd.Flags().Bool("json", false, "Output as JSON") 134 + viewCmd.Flags().Bool("no-metadata", false, "Hide creation/modification timestamps") 135 + root.AddCommand(viewCmd) 136 137 root.AddCommand(&cobra.Command{ 138 Use: "update [task-id] [options...]", 139 Short: "Update task properties", 140 Args: cobra.MinimumNArgs(1), 141 RunE: func(cmd *cobra.Command, args []string) error { 142 + handler, err := handlers.NewTaskHandler() 143 + if err != nil { 144 + return err 145 + } 146 + defer handler.Close() 147 + return handler.Update(cmd.Context(), args) 148 }, 149 }) 150 ··· 153 Short: "Delete a task", 154 Args: cobra.ExactArgs(1), 155 RunE: func(cmd *cobra.Command, args []string) error { 156 + handler, err := handlers.NewTaskHandler() 157 + if err != nil { 158 + return err 159 + } 160 + defer handler.Close() 161 + return handler.Delete(cmd.Context(), args) 162 }, 163 }) 164 ··· 168 Aliases: []string{"proj"}, 169 RunE: func(cmd *cobra.Command, args []string) error { 170 static, _ := cmd.Flags().GetBool("static") 171 + handler, err := handlers.NewTaskHandler() 172 + if err != nil { 173 + return err 174 + } 175 + defer handler.Close() 176 + return handler.ListProjects(cmd.Context(), static) 177 }, 178 } 179 projectsCmd.Flags().Bool("static", false, "Use static text output instead of interactive") ··· 185 Aliases: []string{"t"}, 186 RunE: func(cmd *cobra.Command, args []string) error { 187 static, _ := cmd.Flags().GetBool("static") 188 + handler, err := handlers.NewTaskHandler() 189 + if err != nil { 190 + return err 191 + } 192 + defer handler.Close() 193 + return handler.ListTags(cmd.Context(), static) 194 }, 195 } 196 tagsCmd.Flags().Bool("static", false, "Use static text output instead of interactive") ··· 212 Aliases: []string{"complete"}, 213 Args: cobra.ExactArgs(1), 214 RunE: func(cmd *cobra.Command, args []string) error { 215 + handler, err := handlers.NewTaskHandler() 216 + if err != nil { 217 + return err 218 + } 219 + defer handler.Close() 220 + return handler.Done(cmd.Context(), args) 221 }, 222 }) 223 ··· 250 RunE: func(cmd *cobra.Command, args []string) error { 251 title := args[0] 252 fmt.Printf("Adding movie: %s\n", title) 253 return nil 254 }, 255 }) ··· 259 Short: "List movies in queue", 260 RunE: func(cmd *cobra.Command, args []string) error { 261 fmt.Println("Listing movies...") 262 return nil 263 }, 264 }) ··· 270 Args: cobra.ExactArgs(1), 271 RunE: func(cmd *cobra.Command, args []string) error { 272 fmt.Printf("Marking movie %s as watched\n", args[0]) 273 return nil 274 }, 275 }) ··· 281 Args: cobra.ExactArgs(1), 282 RunE: func(cmd *cobra.Command, args []string) error { 283 fmt.Printf("Removing movie %s from queue\n", args[0]) 284 return nil 285 }, 286 }) ··· 301 RunE: func(cmd *cobra.Command, args []string) error { 302 title := args[0] 303 fmt.Printf("Adding TV show: %s\n", title) 304 return nil 305 }, 306 }) ··· 310 Short: "List TV shows in queue", 311 RunE: func(cmd *cobra.Command, args []string) error { 312 fmt.Println("Listing TV shows...") 313 return nil 314 }, 315 }) ··· 321 Args: cobra.ExactArgs(1), 322 RunE: func(cmd *cobra.Command, args []string) error { 323 fmt.Printf("Marking TV show %s as watched\n", args[0]) 324 return nil 325 }, 326 }) ··· 332 Args: cobra.ExactArgs(1), 333 RunE: func(cmd *cobra.Command, args []string) error { 334 fmt.Printf("Removing TV show %s from queue\n", args[0]) 335 return nil 336 }, 337 }) ··· 607 RunE: func(cmd *cobra.Command, args []string) error { 608 key, value := args[0], args[1] 609 fmt.Printf("Setting config %s = %s\n", key, value) 610 return nil 611 }, 612 }
+2 -2
go.mod
··· 36 github.com/yuin/goldmark v1.7.8 // indirect 37 github.com/yuin/goldmark-emoji v1.0.5 // indirect 38 golang.org/x/net v0.33.0 // indirect 39 - golang.org/x/sync v0.13.0 // indirect 40 golang.org/x/term v0.31.0 // indirect 41 ) 42 ··· 67 github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect 68 golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect 69 golang.org/x/sys v0.33.0 // indirect 70 - golang.org/x/text v0.24.0 71 )
··· 36 github.com/yuin/goldmark v1.7.8 // indirect 37 github.com/yuin/goldmark-emoji v1.0.5 // indirect 38 golang.org/x/net v0.33.0 // indirect 39 + golang.org/x/sync v0.16.0 // indirect 40 golang.org/x/term v0.31.0 // indirect 41 ) 42 ··· 67 github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect 68 golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect 69 golang.org/x/sys v0.33.0 // indirect 70 + golang.org/x/text v0.28.0 71 )
+4 -4
go.sum
··· 134 golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= 135 golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= 136 golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= 137 - golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= 138 - golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 139 golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 140 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 141 golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= 142 golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 143 golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= 144 golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= 145 - golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= 146 - golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= 147 golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= 148 golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= 149 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
··· 134 golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= 135 golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= 136 golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= 137 + golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= 138 + golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 139 golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 140 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 141 golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= 142 golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 143 golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= 144 golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= 145 + golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= 146 + golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= 147 golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= 148 golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= 149 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+42 -58
internal/handlers/notes.go
··· 68 return h.createFromArgs(ctx, title, content) 69 } 70 71 - // Edit handles note editing by ID 72 - func (h *NoteHandler) Edit(ctx context.Context, noteID int64) error { 73 - return h.editNote(ctx, noteID) 74 - } 75 - 76 - // View displays a note with formatted markdown content 77 - func (h *NoteHandler) View(ctx context.Context, noteID int64) error { 78 - return h.viewNote(ctx, noteID) 79 - } 80 - 81 - // List opens either an interactive TUI browser for navigating and viewing notes or a static list 82 - func (h *NoteHandler) List(ctx context.Context, static, showArchived bool, tags []string) error { 83 - return h.listNotes(ctx, showArchived, tags, static) 84 - } 85 - 86 - // Delete permanently removes a note and its metadata 87 - func (h *NoteHandler) Delete(ctx context.Context, noteID int64) error { 88 - return h.deleteNote(ctx, noteID) 89 - } 90 - 91 func (h *NoteHandler) createInteractive(ctx context.Context) error { 92 logger := utils.GetLogger() 93 ··· 213 var response string 214 fmt.Scanln(&response) 215 if strings.ToLower(response) == "y" || strings.ToLower(response) == "yes" { 216 - return h.editNote(ctx, id) 217 } 218 } 219 220 return nil 221 } 222 223 - func (h *NoteHandler) editNote(ctx context.Context, id int64) error { 224 note, err := h.repos.Notes.Get(ctx, id) 225 if err != nil { 226 return fmt.Errorf("failed to get note: %w", err) ··· 355 return content.String() 356 } 357 358 - func (h *NoteHandler) viewNote(ctx context.Context, id int64) error { 359 note, err := h.repos.Notes.Get(ctx, id) 360 if err != nil { 361 return fmt.Errorf("failed to get note: %w", err) ··· 379 return nil 380 } 381 382 - func (h *NoteHandler) formatNoteForView(note *models.Note) string { 383 - var content strings.Builder 384 - 385 - content.WriteString("# " + note.Title + "\n\n") 386 - 387 - if len(note.Tags) > 0 { 388 - content.WriteString("**Tags:** ") 389 - for i, tag := range note.Tags { 390 - if i > 0 { 391 - content.WriteString(", ") 392 - } 393 - content.WriteString("`" + tag + "`") 394 - } 395 - content.WriteString("\n\n") 396 - } 397 - 398 - content.WriteString("**Created:** " + note.Created.Format("2006-01-02 15:04") + "\n") 399 - content.WriteString("**Modified:** " + note.Modified.Format("2006-01-02 15:04") + "\n\n") 400 - content.WriteString("---\n\n") 401 - 402 - noteContent := strings.TrimSpace(note.Content) 403 - if !strings.HasPrefix(noteContent, "# ") { 404 - content.WriteString(noteContent) 405 - } else { 406 - lines := strings.Split(noteContent, "\n") 407 - if len(lines) > 1 { 408 - content.WriteString(strings.Join(lines[1:], "\n")) 409 - } 410 - } 411 - 412 - return content.String() 413 - } 414 - 415 - func (h *NoteHandler) listNotes(ctx context.Context, showArchived bool, tags []string, static bool) error { 416 opts := ui.NoteListOptions{ 417 Output: os.Stdout, 418 Input: os.Stdin, ··· 425 return noteList.Browse(ctx) 426 } 427 428 - func (h *NoteHandler) deleteNote(ctx context.Context, id int64) error { 429 note, err := h.repos.Notes.Get(ctx, id) 430 if err != nil { 431 return fmt.Errorf("failed to find note: %w", err) ··· 447 } 448 return nil 449 }
··· 68 return h.createFromArgs(ctx, title, content) 69 } 70 71 func (h *NoteHandler) createInteractive(ctx context.Context) error { 72 logger := utils.GetLogger() 73 ··· 193 var response string 194 fmt.Scanln(&response) 195 if strings.ToLower(response) == "y" || strings.ToLower(response) == "yes" { 196 + return h.Edit(ctx, id) 197 } 198 } 199 200 return nil 201 } 202 203 + // Edit handles note editing by ID 204 + func (h *NoteHandler) Edit(ctx context.Context, id int64) error { 205 note, err := h.repos.Notes.Get(ctx, id) 206 if err != nil { 207 return fmt.Errorf("failed to get note: %w", err) ··· 336 return content.String() 337 } 338 339 + // View displays a note with formatted markdown content 340 + func (h *NoteHandler) View(ctx context.Context, id int64) error { 341 note, err := h.repos.Notes.Get(ctx, id) 342 if err != nil { 343 return fmt.Errorf("failed to get note: %w", err) ··· 361 return nil 362 } 363 364 + // List opens either an interactive TUI browser for navigating and viewing notes or a static list 365 + func (h *NoteHandler) List(ctx context.Context, static, showArchived bool, tags []string) error { 366 opts := ui.NoteListOptions{ 367 Output: os.Stdout, 368 Input: os.Stdin, ··· 375 return noteList.Browse(ctx) 376 } 377 378 + // Delete permanently removes a note and its metadata 379 + func (h *NoteHandler) Delete(ctx context.Context, id int64) error { 380 note, err := h.repos.Notes.Get(ctx, id) 381 if err != nil { 382 return fmt.Errorf("failed to find note: %w", err) ··· 398 } 399 return nil 400 } 401 + 402 + func (h *NoteHandler) formatNoteForView(note *models.Note) string { 403 + var content strings.Builder 404 + 405 + content.WriteString("# " + note.Title + "\n\n") 406 + 407 + if len(note.Tags) > 0 { 408 + content.WriteString("**Tags:** ") 409 + for i, tag := range note.Tags { 410 + if i > 0 { 411 + content.WriteString(", ") 412 + } 413 + content.WriteString("`" + tag + "`") 414 + } 415 + content.WriteString("\n\n") 416 + } 417 + 418 + content.WriteString("**Created:** " + note.Created.Format("2006-01-02 15:04") + "\n") 419 + content.WriteString("**Modified:** " + note.Modified.Format("2006-01-02 15:04") + "\n\n") 420 + content.WriteString("---\n\n") 421 + 422 + noteContent := strings.TrimSpace(note.Content) 423 + if !strings.HasPrefix(noteContent, "# ") { 424 + content.WriteString(noteContent) 425 + } else { 426 + lines := strings.Split(noteContent, "\n") 427 + if len(lines) > 1 { 428 + content.WriteString(strings.Join(lines[1:], "\n")) 429 + } 430 + } 431 + 432 + return content.String() 433 + }
+81 -100
internal/handlers/tasks.go
··· 2 3 import ( 4 "context" 5 "fmt" 6 "slices" 7 "strconv" ··· 13 "github.com/stormlightlabs/noteleaf/internal/repo" 14 "github.com/stormlightlabs/noteleaf/internal/store" 15 "github.com/stormlightlabs/noteleaf/internal/ui" 16 ) 17 18 // TaskHandler handles all task-related commands ··· 48 return h.db.Close() 49 } 50 51 - // CreateTask creates a new task 52 - func CreateTask(ctx context.Context, args []string) error { 53 - handler, err := NewTaskHandler() 54 - if err != nil { 55 - return fmt.Errorf("failed to initialize task handler: %w", err) 56 - } 57 - defer handler.Close() 58 - 59 - return handler.createTask(ctx, args) 60 - } 61 - 62 - func (h *TaskHandler) createTask(ctx context.Context, args []string) error { 63 - if len(args) < 1 { 64 return fmt.Errorf("task description required") 65 } 66 67 - description := strings.Join(args, " ") 68 69 task := &models.Task{ 70 UUID: uuid.New().String(), 71 Description: description, 72 Status: "pending", 73 } 74 75 id, err := h.repos.Tasks.Create(ctx, task) ··· 78 } 79 80 fmt.Printf("Task created (ID: %d, UUID: %s): %s\n", id, task.UUID, task.Description) 81 return nil 82 } 83 84 - // ListTasks lists all tasks with optional filtering 85 - func ListTasks(ctx context.Context, static, showAll bool, status, priority, project string) error { 86 - handler, err := NewTaskHandler() 87 - if err != nil { 88 - return fmt.Errorf("failed to initialize task handler: %w", err) 89 - } 90 - defer handler.Close() 91 - 92 if static { 93 - return handler.listTasksStatic(ctx, showAll, status, priority, project) 94 } 95 96 - return handler.listTasksInteractive(ctx, showAll, status, priority, project) 97 } 98 99 func (h *TaskHandler) listTasksStatic(ctx context.Context, showAll bool, status, priority, project string) error { ··· 137 return taskList.Browse(ctx) 138 } 139 140 - // UpdateTask updates an existing task 141 - func UpdateTask(ctx context.Context, args []string) error { 142 - handler, err := NewTaskHandler() 143 - if err != nil { 144 - return fmt.Errorf("failed to initialize task handler: %w", err) 145 - } 146 - defer handler.Close() 147 - 148 - return handler.updateTask(ctx, args) 149 - } 150 - 151 - func (h *TaskHandler) updateTask(ctx context.Context, args []string) error { 152 if len(args) < 1 { 153 return fmt.Errorf("task ID required") 154 } ··· 207 return nil 208 } 209 210 - // DeleteTask deletes a task 211 - func DeleteTask(ctx context.Context, args []string) error { 212 - handler, err := NewTaskHandler() 213 - if err != nil { 214 - return fmt.Errorf("failed to initialize task handler: %w", err) 215 - } 216 - defer handler.Close() 217 - 218 - return handler.deleteTask(ctx, args) 219 - } 220 - 221 - func (h *TaskHandler) deleteTask(ctx context.Context, args []string) error { 222 if len(args) < 1 { 223 return fmt.Errorf("task ID required") 224 } ··· 251 return nil 252 } 253 254 - // ViewTask displays a single task 255 - func ViewTask(ctx context.Context, args []string) error { 256 - handler, err := NewTaskHandler() 257 - if err != nil { 258 - return fmt.Errorf("failed to initialize task handler: %w", err) 259 - } 260 - defer handler.Close() 261 - 262 - return handler.viewTask(ctx, args) 263 - } 264 - 265 - func (h *TaskHandler) viewTask(ctx context.Context, args []string) error { 266 if len(args) < 1 { 267 return fmt.Errorf("task ID required") 268 } ··· 281 return fmt.Errorf("failed to find task: %w", err) 282 } 283 284 - h.printTaskDetail(task) 285 - return nil 286 - } 287 288 - // DoneTask marks a task as completed 289 - func DoneTask(ctx context.Context, args []string) error { 290 - handler, err := NewTaskHandler() 291 - if err != nil { 292 - return fmt.Errorf("failed to initialize task handler: %w", err) 293 } 294 - defer handler.Close() 295 - 296 - return handler.doneTask(ctx, args) 297 } 298 299 - func (h *TaskHandler) doneTask(ctx context.Context, args []string) error { 300 if len(args) < 1 { 301 return fmt.Errorf("task ID required") 302 } ··· 334 } 335 336 // ListProjects lists all projects with their task counts 337 - func ListProjects(ctx context.Context, static bool) error { 338 - handler, err := NewTaskHandler() 339 - if err != nil { 340 - return fmt.Errorf("failed to initialize task handler: %w", err) 341 - } 342 - defer handler.Close() 343 - 344 if static { 345 - return handler.listProjectsStatic(ctx) 346 } 347 348 - return handler.listProjectsInteractive(ctx) 349 } 350 351 func (h *TaskHandler) listProjectsStatic(ctx context.Context) error { ··· 387 } 388 389 // ListTags lists all tags with their task counts 390 - func ListTags(ctx context.Context, static bool) error { 391 - handler, err := NewTaskHandler() 392 - if err != nil { 393 - return fmt.Errorf("failed to initialize task handler: %w", err) 394 - } 395 - defer handler.Close() 396 - 397 if static { 398 - return handler.listTagsStatic(ctx) 399 } 400 401 - return handler.listTagsInteractive(ctx) 402 } 403 404 func (h *TaskHandler) listTagsStatic(ctx context.Context) error { ··· 465 fmt.Println() 466 } 467 468 - func (h *TaskHandler) printTaskDetail(task *models.Task) { 469 fmt.Printf("Task ID: %d\n", task.ID) 470 fmt.Printf("UUID: %s\n", task.UUID) 471 fmt.Printf("Description: %s\n", task.Description) ··· 487 fmt.Printf("Due: %s\n", task.Due.Format("2006-01-02 15:04")) 488 } 489 490 - fmt.Printf("Created: %s\n", task.Entry.Format("2006-01-02 15:04")) 491 - fmt.Printf("Modified: %s\n", task.Modified.Format("2006-01-02 15:04")) 492 493 - if task.Start != nil { 494 - fmt.Printf("Started: %s\n", task.Start.Format("2006-01-02 15:04")) 495 - } 496 497 - if task.End != nil { 498 - fmt.Printf("Completed: %s\n", task.End.Format("2006-01-02 15:04")) 499 } 500 501 if len(task.Annotations) > 0 { ··· 506 } 507 } 508 509 func removeString(slice []string, item string) []string { 510 var result []string 511 for _, s := range slice { ··· 517 } 518 519 func pluralize(count int) string { 520 - if count == 1 { 521 return "" 522 } 523 - return "s" 524 }
··· 2 3 import ( 4 "context" 5 + "encoding/json" 6 "fmt" 7 "slices" 8 "strconv" ··· 14 "github.com/stormlightlabs/noteleaf/internal/repo" 15 "github.com/stormlightlabs/noteleaf/internal/store" 16 "github.com/stormlightlabs/noteleaf/internal/ui" 17 + "golang.org/x/text/feature/plural" 18 + "golang.org/x/text/language" 19 ) 20 21 // TaskHandler handles all task-related commands ··· 51 return h.db.Close() 52 } 53 54 + // Create creates a new task 55 + func (h *TaskHandler) Create(ctx context.Context, desc []string, priority, project, due string, tags []string) error { 56 + if len(desc) < 1 { 57 return fmt.Errorf("task description required") 58 } 59 60 + description := strings.Join(desc, " ") 61 62 task := &models.Task{ 63 UUID: uuid.New().String(), 64 Description: description, 65 Status: "pending", 66 + Priority: priority, 67 + Project: project, 68 + Tags: tags, 69 + } 70 + 71 + if due != "" { 72 + if dueTime, err := time.Parse("2006-01-02", due); err == nil { 73 + task.Due = &dueTime 74 + } else { 75 + return fmt.Errorf("invalid due date format, use YYYY-MM-DD: %w", err) 76 + } 77 } 78 79 id, err := h.repos.Tasks.Create(ctx, task) ··· 82 } 83 84 fmt.Printf("Task created (ID: %d, UUID: %s): %s\n", id, task.UUID, task.Description) 85 + 86 + if priority != "" { 87 + fmt.Printf("Priority: %s\n", priority) 88 + } 89 + if project != "" { 90 + fmt.Printf("Project: %s\n", project) 91 + } 92 + if len(tags) > 0 { 93 + fmt.Printf("Tags: %s\n", strings.Join(tags, ", ")) 94 + } 95 + if task.Due != nil { 96 + fmt.Printf("Due: %s\n", task.Due.Format("2006-01-02")) 97 + } 98 + 99 return nil 100 } 101 102 + // List lists all tasks with optional filtering 103 + func (h *TaskHandler) List(ctx context.Context, static, showAll bool, status, priority, project string) error { 104 if static { 105 + return h.listTasksStatic(ctx, showAll, status, priority, project) 106 } 107 108 + return h.listTasksInteractive(ctx, showAll, status, priority, project) 109 } 110 111 func (h *TaskHandler) listTasksStatic(ctx context.Context, showAll bool, status, priority, project string) error { ··· 149 return taskList.Browse(ctx) 150 } 151 152 + func (h *TaskHandler) Update(ctx context.Context, args []string) error { 153 if len(args) < 1 { 154 return fmt.Errorf("task ID required") 155 } ··· 208 return nil 209 } 210 211 + // Delete deletes a task 212 + func (h *TaskHandler) Delete(ctx context.Context, args []string) error { 213 if len(args) < 1 { 214 return fmt.Errorf("task ID required") 215 } ··· 242 return nil 243 } 244 245 + // View displays a single task 246 + func (h *TaskHandler) View(ctx context.Context, args []string, format string, jsonOutput, noMetadata bool) error { 247 if len(args) < 1 { 248 return fmt.Errorf("task ID required") 249 } ··· 262 return fmt.Errorf("failed to find task: %w", err) 263 } 264 265 + if jsonOutput { 266 + return h.printTaskJSON(task) 267 + } 268 269 + if format == "brief" { 270 + h.printTask(task) 271 + } else { 272 + h.printTaskDetail(task, noMetadata) 273 } 274 + return nil 275 } 276 277 + // Done marks a task as completed 278 + func (h *TaskHandler) Done(ctx context.Context, args []string) error { 279 if len(args) < 1 { 280 return fmt.Errorf("task ID required") 281 } ··· 313 } 314 315 // ListProjects lists all projects with their task counts 316 + func (h *TaskHandler) ListProjects(ctx context.Context, static bool) error { 317 if static { 318 + return h.listProjectsStatic(ctx) 319 } 320 321 + return h.listProjectsInteractive(ctx) 322 } 323 324 func (h *TaskHandler) listProjectsStatic(ctx context.Context) error { ··· 360 } 361 362 // ListTags lists all tags with their task counts 363 + func (h *TaskHandler) ListTags(ctx context.Context, static bool) error { 364 if static { 365 + return h.listTagsStatic(ctx) 366 } 367 368 + return h.listTagsInteractive(ctx) 369 } 370 371 func (h *TaskHandler) listTagsStatic(ctx context.Context) error { ··· 432 fmt.Println() 433 } 434 435 + func (h *TaskHandler) printTaskDetail(task *models.Task, noMetadata bool) { 436 fmt.Printf("Task ID: %d\n", task.ID) 437 fmt.Printf("UUID: %s\n", task.UUID) 438 fmt.Printf("Description: %s\n", task.Description) ··· 454 fmt.Printf("Due: %s\n", task.Due.Format("2006-01-02 15:04")) 455 } 456 457 + if !noMetadata { 458 + fmt.Printf("Created: %s\n", task.Entry.Format("2006-01-02 15:04")) 459 + fmt.Printf("Modified: %s\n", task.Modified.Format("2006-01-02 15:04")) 460 461 + if task.Start != nil { 462 + fmt.Printf("Started: %s\n", task.Start.Format("2006-01-02 15:04")) 463 + } 464 465 + if task.End != nil { 466 + fmt.Printf("Completed: %s\n", task.End.Format("2006-01-02 15:04")) 467 + } 468 } 469 470 if len(task.Annotations) > 0 { ··· 475 } 476 } 477 478 + func (h *TaskHandler) printTaskJSON(task *models.Task) error { 479 + jsonData, err := json.MarshalIndent(task, "", " ") 480 + if err != nil { 481 + return fmt.Errorf("failed to marshal task to JSON: %w", err) 482 + } 483 + fmt.Println(string(jsonData)) 484 + return nil 485 + } 486 + 487 func removeString(slice []string, item string) []string { 488 var result []string 489 for _, s := range slice { ··· 495 } 496 497 func pluralize(count int) string { 498 + rule := plural.Cardinal.MatchPlural(language.English, count, 0, 0, 0, 0) 499 + switch rule { 500 + case plural.One: 501 return "" 502 + default: 503 + return "s" 504 } 505 }
+138 -39
internal/handlers/tasks_test.go
··· 93 _, cleanup := setupTaskTest(t) 94 defer cleanup() 95 96 t.Run("creates task successfully", func(t *testing.T) { 97 ctx := context.Background() 98 args := []string{"Buy groceries", "and", "cook dinner"} 99 100 - err := CreateTask(ctx, args) 101 if err != nil { 102 t.Errorf("CreateTask failed: %v", err) 103 } 104 105 - handler, err := NewTaskHandler() 106 - if err != nil { 107 - t.Fatalf("Failed to create handler: %v", err) 108 - } 109 - defer handler.Close() 110 - 111 tasks, err := handler.repos.Tasks.GetPending(ctx) 112 if err != nil { 113 t.Fatalf("Failed to get pending tasks: %v", err) ··· 136 ctx := context.Background() 137 args := []string{} 138 139 - err := CreateTask(ctx, args) 140 if err == nil { 141 t.Error("Expected error for empty description") 142 } ··· 145 t.Errorf("Expected error about required description, got: %v", err) 146 } 147 }) 148 }) 149 150 t.Run("List", func(t *testing.T) { ··· 182 } 183 184 t.Run("lists pending tasks by default (static mode)", func(t *testing.T) { 185 - err := ListTasks(ctx, true, false, "", "", "") 186 if err != nil { 187 t.Errorf("ListTasks failed: %v", err) 188 } 189 }) 190 191 t.Run("filters by status (static mode)", func(t *testing.T) { 192 - err := ListTasks(ctx, true, false, "completed", "", "") 193 if err != nil { 194 t.Errorf("ListTasks with status filter failed: %v", err) 195 } 196 }) 197 198 t.Run("filters by priority (static mode)", func(t *testing.T) { 199 - err := ListTasks(ctx, true, false, "", "A", "") 200 if err != nil { 201 t.Errorf("ListTasks with priority filter failed: %v", err) 202 } 203 }) 204 205 t.Run("filters by project (static mode)", func(t *testing.T) { 206 - err := ListTasks(ctx, true, false, "", "", "work") 207 if err != nil { 208 t.Errorf("ListTasks with project filter failed: %v", err) 209 } 210 }) 211 212 t.Run("show all tasks (static mode)", func(t *testing.T) { 213 - err := ListTasks(ctx, true, true, "", "", "") 214 if err != nil { 215 t.Errorf("ListTasks with show all failed: %v", err) 216 } ··· 223 224 ctx := context.Background() 225 226 - // Create test task 227 handler, err := NewTaskHandler() 228 if err != nil { 229 t.Fatalf("Failed to create handler: %v", err) ··· 243 t.Run("updates task by ID", func(t *testing.T) { 244 args := []string{strconv.FormatInt(id, 10), "--description", "Updated description"} 245 246 - err := UpdateTask(ctx, args) 247 if err != nil { 248 t.Errorf("UpdateTask failed: %v", err) 249 } ··· 261 t.Run("updates task by UUID", func(t *testing.T) { 262 args := []string{task.UUID, "--status", "completed"} 263 264 - err := UpdateTask(ctx, args) 265 if err != nil { 266 t.Errorf("UpdateTask by UUID failed: %v", err) 267 } ··· 285 "--due", "2024-12-31", 286 } 287 288 - err := UpdateTask(ctx, args) 289 if err != nil { 290 t.Errorf("UpdateTask with multiple fields failed: %v", err) 291 } ··· 317 "--add-tag=urgent", 318 } 319 320 - err := UpdateTask(ctx, args) 321 if err != nil { 322 t.Errorf("UpdateTask with add tags failed: %v", err) 323 } ··· 336 "--remove-tag=urgent", 337 } 338 339 - err = UpdateTask(ctx, args) 340 if err != nil { 341 t.Errorf("UpdateTask with remove tag failed: %v", err) 342 } ··· 358 t.Run("fails with missing task ID", func(t *testing.T) { 359 args := []string{} 360 361 - err := UpdateTask(ctx, args) 362 if err == nil { 363 t.Error("Expected error for missing task ID") 364 } ··· 371 t.Run("fails with invalid task ID", func(t *testing.T) { 372 args := []string{"99999", "--description", "test"} 373 374 - err := UpdateTask(ctx, args) 375 if err == nil { 376 t.Error("Expected error for invalid task ID") 377 } ··· 407 t.Run("deletes task by ID", func(t *testing.T) { 408 args := []string{strconv.FormatInt(id, 10)} 409 410 - err := DeleteTask(ctx, args) 411 if err != nil { 412 t.Errorf("DeleteTask failed: %v", err) 413 } ··· 431 432 args := []string{task2.UUID} 433 434 - err = DeleteTask(ctx, args) 435 if err != nil { 436 t.Errorf("DeleteTask by UUID failed: %v", err) 437 } ··· 445 t.Run("fails with missing task ID", func(t *testing.T) { 446 args := []string{} 447 448 - err := DeleteTask(ctx, args) 449 if err == nil { 450 t.Error("Expected error for missing task ID") 451 } ··· 458 t.Run("fails with invalid task ID", func(t *testing.T) { 459 args := []string{"99999"} 460 461 - err := DeleteTask(ctx, args) 462 if err == nil { 463 t.Error("Expected error for invalid task ID") 464 } ··· 500 t.Run("views task by ID", func(t *testing.T) { 501 args := []string{strconv.FormatInt(id, 10)} 502 503 - err := ViewTask(ctx, args) 504 if err != nil { 505 t.Errorf("ViewTask failed: %v", err) 506 } ··· 509 t.Run("views task by UUID", func(t *testing.T) { 510 args := []string{task.UUID} 511 512 - err := ViewTask(ctx, args) 513 if err != nil { 514 t.Errorf("ViewTask by UUID failed: %v", err) 515 } ··· 518 t.Run("fails with missing task ID", func(t *testing.T) { 519 args := []string{} 520 521 - err := ViewTask(ctx, args) 522 if err == nil { 523 t.Error("Expected error for missing task ID") 524 } ··· 531 t.Run("fails with invalid task ID", func(t *testing.T) { 532 args := []string{"99999"} 533 534 - err := ViewTask(ctx, args) 535 if err == nil { 536 t.Error("Expected error for invalid task ID") 537 } ··· 540 t.Errorf("Expected error about task not found, got: %v", err) 541 } 542 }) 543 }) 544 545 t.Run("Done", func(t *testing.T) { ··· 567 t.Run("marks task as done by ID", func(t *testing.T) { 568 args := []string{strconv.FormatInt(id, 10)} 569 570 - err := DoneTask(ctx, args) 571 if err != nil { 572 t.Errorf("DoneTask failed: %v", err) 573 } ··· 599 600 args := []string{strconv.FormatInt(id2, 10)} 601 602 - err = DoneTask(ctx, args) 603 if err != nil { 604 t.Errorf("DoneTask on completed task failed: %v", err) 605 } ··· 618 619 args := []string{task3.UUID} 620 621 - err = DoneTask(ctx, args) 622 if err != nil { 623 t.Errorf("DoneTask by UUID failed: %v", err) 624 } ··· 640 t.Run("fails with missing task ID", func(t *testing.T) { 641 args := []string{} 642 643 - err := DoneTask(ctx, args) 644 if err == nil { 645 t.Error("Expected error for missing task ID") 646 } ··· 653 t.Run("fails with invalid task ID", func(t *testing.T) { 654 args := []string{"99999"} 655 656 - err := DoneTask(ctx, args) 657 if err == nil { 658 t.Error("Expected error for invalid task ID") 659 } ··· 726 } 727 }() 728 729 - handler.printTaskDetail(task) 730 }) 731 }) 732 ··· 757 } 758 759 t.Run("lists projects successfully", func(t *testing.T) { 760 - err := ListProjects(ctx, true) 761 if err != nil { 762 t.Errorf("ListProjects failed: %v", err) 763 } ··· 767 _, cleanup2 := setupTaskTest(t) 768 defer cleanup2() 769 770 - err := ListProjects(ctx, true) 771 if err != nil { 772 t.Errorf("ListProjects with no projects failed: %v", err) 773 } ··· 801 } 802 803 t.Run("lists tags successfully", func(t *testing.T) { 804 - err := ListTags(ctx, true) 805 if err != nil { 806 t.Errorf("ListTags failed: %v", err) 807 } ··· 811 _, cleanup2 := setupTaskTest(t) 812 defer cleanup2() 813 814 - err := ListTags(ctx, true) 815 if err != nil { 816 t.Errorf("ListTags with no tags failed: %v", err) 817 }
··· 93 _, cleanup := setupTaskTest(t) 94 defer cleanup() 95 96 + handler, err := NewTaskHandler() 97 + if err != nil { 98 + t.Fatalf("Failed to create handler: %v", err) 99 + } 100 + defer handler.Close() 101 + 102 t.Run("creates task successfully", func(t *testing.T) { 103 ctx := context.Background() 104 args := []string{"Buy groceries", "and", "cook dinner"} 105 106 + err := handler.Create(ctx, args, "", "", "", []string{}) 107 if err != nil { 108 t.Errorf("CreateTask failed: %v", err) 109 } 110 111 tasks, err := handler.repos.Tasks.GetPending(ctx) 112 if err != nil { 113 t.Fatalf("Failed to get pending tasks: %v", err) ··· 136 ctx := context.Background() 137 args := []string{} 138 139 + err := handler.Create(ctx, args, "", "", "", []string{}) 140 if err == nil { 141 t.Error("Expected error for empty description") 142 } ··· 145 t.Errorf("Expected error about required description, got: %v", err) 146 } 147 }) 148 + 149 + t.Run("creates task with flags", func(t *testing.T) { 150 + ctx := context.Background() 151 + args := []string{"Task", "with", "flags"} 152 + priority := "A" 153 + project := "test-project" 154 + due := "2024-12-31" 155 + tags := []string{"urgent", "work"} 156 + 157 + err := handler.Create(ctx, args, priority, project, due, tags) 158 + if err != nil { 159 + t.Errorf("CreateTask with flags failed: %v", err) 160 + } 161 + 162 + tasks, err := handler.repos.Tasks.GetPending(ctx) 163 + if err != nil { 164 + t.Fatalf("Failed to get pending tasks: %v", err) 165 + } 166 + 167 + // Should have 2 tasks now (previous test created one) 168 + if len(tasks) < 1 { 169 + t.Errorf("Expected at least 1 task, got %d", len(tasks)) 170 + } 171 + 172 + // Find the task we just created 173 + var task *models.Task 174 + for _, t := range tasks { 175 + if t.Description == "Task with flags" { 176 + task = t 177 + break 178 + } 179 + } 180 + 181 + if task == nil { 182 + t.Fatal("Could not find created task") 183 + } 184 + 185 + if task.Priority != priority { 186 + t.Errorf("Expected priority '%s', got '%s'", priority, task.Priority) 187 + } 188 + 189 + if task.Project != project { 190 + t.Errorf("Expected project '%s', got '%s'", project, task.Project) 191 + } 192 + 193 + if task.Due == nil { 194 + t.Error("Expected due date to be set") 195 + } else if task.Due.Format("2006-01-02") != due { 196 + t.Errorf("Expected due date '%s', got '%s'", due, task.Due.Format("2006-01-02")) 197 + } 198 + 199 + if len(task.Tags) != len(tags) { 200 + t.Errorf("Expected %d tags, got %d", len(tags), len(task.Tags)) 201 + } else { 202 + for i, tag := range tags { 203 + if task.Tags[i] != tag { 204 + t.Errorf("Expected tag '%s' at index %d, got '%s'", tag, i, task.Tags[i]) 205 + } 206 + } 207 + } 208 + }) 209 + 210 + t.Run("fails with invalid due date format", func(t *testing.T) { 211 + ctx := context.Background() 212 + args := []string{"Task", "with", "invalid", "date"} 213 + invalidDue := "invalid-date" 214 + 215 + err := handler.Create(ctx, args, "", "", invalidDue, []string{}) 216 + if err == nil { 217 + t.Error("Expected error for invalid due date format") 218 + } 219 + 220 + if !strings.Contains(err.Error(), "invalid due date format") { 221 + t.Errorf("Expected error about invalid date format, got: %v", err) 222 + } 223 + }) 224 }) 225 226 t.Run("List", func(t *testing.T) { ··· 258 } 259 260 t.Run("lists pending tasks by default (static mode)", func(t *testing.T) { 261 + err := handler.List(ctx, true, false, "", "", "") 262 if err != nil { 263 t.Errorf("ListTasks failed: %v", err) 264 } 265 }) 266 267 t.Run("filters by status (static mode)", func(t *testing.T) { 268 + err := handler.List(ctx, true, false, "completed", "", "") 269 if err != nil { 270 t.Errorf("ListTasks with status filter failed: %v", err) 271 } 272 }) 273 274 t.Run("filters by priority (static mode)", func(t *testing.T) { 275 + err := handler.List(ctx, true, false, "", "A", "") 276 if err != nil { 277 t.Errorf("ListTasks with priority filter failed: %v", err) 278 } 279 }) 280 281 t.Run("filters by project (static mode)", func(t *testing.T) { 282 + err := handler.List(ctx, true, false, "", "", "work") 283 if err != nil { 284 t.Errorf("ListTasks with project filter failed: %v", err) 285 } 286 }) 287 288 t.Run("show all tasks (static mode)", func(t *testing.T) { 289 + err := handler.List(ctx, true, true, "", "", "") 290 if err != nil { 291 t.Errorf("ListTasks with show all failed: %v", err) 292 } ··· 299 300 ctx := context.Background() 301 302 handler, err := NewTaskHandler() 303 if err != nil { 304 t.Fatalf("Failed to create handler: %v", err) ··· 318 t.Run("updates task by ID", func(t *testing.T) { 319 args := []string{strconv.FormatInt(id, 10), "--description", "Updated description"} 320 321 + err := handler.Update(ctx, args) 322 if err != nil { 323 t.Errorf("UpdateTask failed: %v", err) 324 } ··· 336 t.Run("updates task by UUID", func(t *testing.T) { 337 args := []string{task.UUID, "--status", "completed"} 338 339 + err := handler.Update(ctx, args) 340 if err != nil { 341 t.Errorf("UpdateTask by UUID failed: %v", err) 342 } ··· 360 "--due", "2024-12-31", 361 } 362 363 + err := handler.Update(ctx, args) 364 if err != nil { 365 t.Errorf("UpdateTask with multiple fields failed: %v", err) 366 } ··· 392 "--add-tag=urgent", 393 } 394 395 + err := handler.Update(ctx, args) 396 if err != nil { 397 t.Errorf("UpdateTask with add tags failed: %v", err) 398 } ··· 411 "--remove-tag=urgent", 412 } 413 414 + err = handler.Update(ctx, args) 415 if err != nil { 416 t.Errorf("UpdateTask with remove tag failed: %v", err) 417 } ··· 433 t.Run("fails with missing task ID", func(t *testing.T) { 434 args := []string{} 435 436 + err := handler.Update(ctx, args) 437 if err == nil { 438 t.Error("Expected error for missing task ID") 439 } ··· 446 t.Run("fails with invalid task ID", func(t *testing.T) { 447 args := []string{"99999", "--description", "test"} 448 449 + err := handler.Update(ctx, args) 450 if err == nil { 451 t.Error("Expected error for invalid task ID") 452 } ··· 482 t.Run("deletes task by ID", func(t *testing.T) { 483 args := []string{strconv.FormatInt(id, 10)} 484 485 + err := handler.Delete(ctx, args) 486 if err != nil { 487 t.Errorf("DeleteTask failed: %v", err) 488 } ··· 506 507 args := []string{task2.UUID} 508 509 + err = handler.Delete(ctx, args) 510 if err != nil { 511 t.Errorf("DeleteTask by UUID failed: %v", err) 512 } ··· 520 t.Run("fails with missing task ID", func(t *testing.T) { 521 args := []string{} 522 523 + err := handler.Delete(ctx, args) 524 if err == nil { 525 t.Error("Expected error for missing task ID") 526 } ··· 533 t.Run("fails with invalid task ID", func(t *testing.T) { 534 args := []string{"99999"} 535 536 + err := handler.Delete(ctx, args) 537 if err == nil { 538 t.Error("Expected error for invalid task ID") 539 } ··· 575 t.Run("views task by ID", func(t *testing.T) { 576 args := []string{strconv.FormatInt(id, 10)} 577 578 + err := handler.View(ctx, args, "detailed", false, false) 579 if err != nil { 580 t.Errorf("ViewTask failed: %v", err) 581 } ··· 584 t.Run("views task by UUID", func(t *testing.T) { 585 args := []string{task.UUID} 586 587 + err := handler.View(ctx, args, "detailed", false, false) 588 if err != nil { 589 t.Errorf("ViewTask by UUID failed: %v", err) 590 } ··· 593 t.Run("fails with missing task ID", func(t *testing.T) { 594 args := []string{} 595 596 + err := handler.View(ctx, args, "detailed", false, false) 597 if err == nil { 598 t.Error("Expected error for missing task ID") 599 } ··· 606 t.Run("fails with invalid task ID", func(t *testing.T) { 607 args := []string{"99999"} 608 609 + err := handler.View(ctx, args, "detailed", false, false) 610 if err == nil { 611 t.Error("Expected error for invalid task ID") 612 } ··· 615 t.Errorf("Expected error about task not found, got: %v", err) 616 } 617 }) 618 + 619 + t.Run("uses brief format", func(t *testing.T) { 620 + args := []string{strconv.FormatInt(id, 10)} 621 + err := handler.View(ctx, args, "brief", false, false) 622 + if err != nil { 623 + t.Errorf("ViewTask with brief format failed: %v", err) 624 + } 625 + }) 626 + 627 + t.Run("hides metadata", func(t *testing.T) { 628 + args := []string{strconv.FormatInt(id, 10)} 629 + err := handler.View(ctx, args, "detailed", false, true) 630 + if err != nil { 631 + t.Errorf("ViewTask with no-metadata failed: %v", err) 632 + } 633 + }) 634 + 635 + t.Run("outputs JSON", func(t *testing.T) { 636 + args := []string{strconv.FormatInt(id, 10)} 637 + err := handler.View(ctx, args, "detailed", true, false) 638 + if err != nil { 639 + t.Errorf("ViewTask with JSON output failed: %v", err) 640 + } 641 + }) 642 }) 643 644 t.Run("Done", func(t *testing.T) { ··· 666 t.Run("marks task as done by ID", func(t *testing.T) { 667 args := []string{strconv.FormatInt(id, 10)} 668 669 + err := handler.Done(ctx, args) 670 if err != nil { 671 t.Errorf("DoneTask failed: %v", err) 672 } ··· 698 699 args := []string{strconv.FormatInt(id2, 10)} 700 701 + err = handler.Done(ctx, args) 702 if err != nil { 703 t.Errorf("DoneTask on completed task failed: %v", err) 704 } ··· 717 718 args := []string{task3.UUID} 719 720 + err = handler.Done(ctx, args) 721 if err != nil { 722 t.Errorf("DoneTask by UUID failed: %v", err) 723 } ··· 739 t.Run("fails with missing task ID", func(t *testing.T) { 740 args := []string{} 741 742 + err := handler.Done(ctx, args) 743 if err == nil { 744 t.Error("Expected error for missing task ID") 745 } ··· 752 t.Run("fails with invalid task ID", func(t *testing.T) { 753 args := []string{"99999"} 754 755 + err := handler.Done(ctx, args) 756 if err == nil { 757 t.Error("Expected error for invalid task ID") 758 } ··· 825 } 826 }() 827 828 + handler.printTaskDetail(task, false) 829 }) 830 }) 831 ··· 856 } 857 858 t.Run("lists projects successfully", func(t *testing.T) { 859 + err := handler.ListProjects(ctx, true) 860 if err != nil { 861 t.Errorf("ListProjects failed: %v", err) 862 } ··· 866 _, cleanup2 := setupTaskTest(t) 867 defer cleanup2() 868 869 + err := handler.ListProjects(ctx, true) 870 if err != nil { 871 t.Errorf("ListProjects with no projects failed: %v", err) 872 } ··· 900 } 901 902 t.Run("lists tags successfully", func(t *testing.T) { 903 + err := handler.ListTags(ctx, true) 904 if err != nil { 905 t.Errorf("ListTags failed: %v", err) 906 } ··· 910 _, cleanup2 := setupTaskTest(t) 911 defer cleanup2() 912 913 + err := handler.ListTags(ctx, true) 914 if err != nil { 915 t.Errorf("ListTags with no tags failed: %v", err) 916 }