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