gogcli/internal/cmd/tasks_items.go
2025-12-26 18:32:06 +01:00

448 lines
12 KiB
Go

package cmd
import (
"fmt"
"os"
"strings"
"github.com/spf13/cobra"
"github.com/steipete/gogcli/internal/outfmt"
"github.com/steipete/gogcli/internal/ui"
"google.golang.org/api/tasks/v1"
)
func newTasksListCmd(flags *rootFlags) *cobra.Command {
var max int64
var page string
var showCompleted bool
var showDeleted bool
var showHidden bool
var showAssigned bool
var dueMin string
var dueMax string
var completedMin string
var completedMax string
var updatedMin string
cmd := &cobra.Command{
Use: "list <tasklistId>",
Short: "List tasks in a task list",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
u := ui.FromContext(cmd.Context())
account, err := requireAccount(flags)
if err != nil {
return err
}
tasklistID := strings.TrimSpace(args[0])
if tasklistID == "" {
return usage("empty tasklistId")
}
svc, err := newTasksService(cmd.Context(), account)
if err != nil {
return err
}
call := svc.Tasks.List(tasklistID).
MaxResults(max).
PageToken(page).
ShowCompleted(showCompleted).
ShowDeleted(showDeleted).
ShowHidden(showHidden).
ShowAssigned(showAssigned)
if strings.TrimSpace(dueMin) != "" {
call = call.DueMin(strings.TrimSpace(dueMin))
}
if strings.TrimSpace(dueMax) != "" {
call = call.DueMax(strings.TrimSpace(dueMax))
}
if strings.TrimSpace(completedMin) != "" {
call = call.CompletedMin(strings.TrimSpace(completedMin))
}
if strings.TrimSpace(completedMax) != "" {
call = call.CompletedMax(strings.TrimSpace(completedMax))
}
if strings.TrimSpace(updatedMin) != "" {
call = call.UpdatedMin(strings.TrimSpace(updatedMin))
}
resp, err := call.Do()
if err != nil {
return err
}
if outfmt.IsJSON(cmd.Context()) {
return outfmt.WriteJSON(os.Stdout, map[string]any{
"tasks": resp.Items,
"nextPageToken": resp.NextPageToken,
})
}
if len(resp.Items) == 0 {
u.Err().Println("No tasks")
return nil
}
w, flush := tableWriter(cmd.Context())
defer flush()
fmt.Fprintln(w, "ID\tTITLE\tSTATUS\tDUE\tUPDATED")
for _, t := range resp.Items {
status := strings.TrimSpace(t.Status)
if status == "" {
status = "needsAction"
}
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", t.Id, t.Title, status, strings.TrimSpace(t.Due), strings.TrimSpace(t.Updated))
}
printNextPageHint(u, resp.NextPageToken)
return nil
},
}
cmd.Flags().Int64Var(&max, "max", 20, "Max results (max allowed: 100)")
cmd.Flags().StringVar(&page, "page", "", "Page token")
cmd.Flags().BoolVar(&showCompleted, "show-completed", true, "Include completed tasks (requires --show-hidden for some clients)")
cmd.Flags().BoolVar(&showDeleted, "show-deleted", false, "Include deleted tasks")
cmd.Flags().BoolVar(&showHidden, "show-hidden", false, "Include hidden tasks")
cmd.Flags().BoolVar(&showAssigned, "show-assigned", false, "Include tasks assigned to current user")
cmd.Flags().StringVar(&dueMin, "due-min", "", "Lower bound for due date filter (RFC3339)")
cmd.Flags().StringVar(&dueMax, "due-max", "", "Upper bound for due date filter (RFC3339)")
cmd.Flags().StringVar(&completedMin, "completed-min", "", "Lower bound for completion date filter (RFC3339)")
cmd.Flags().StringVar(&completedMax, "completed-max", "", "Upper bound for completion date filter (RFC3339)")
cmd.Flags().StringVar(&updatedMin, "updated-min", "", "Lower bound for updated time filter (RFC3339)")
return cmd
}
func newTasksAddCmd(flags *rootFlags) *cobra.Command {
var title string
var notes string
var due string
var parent string
var previous string
cmd := &cobra.Command{
Use: "add <tasklistId>",
Short: "Create a task in a task list",
Aliases: []string{"create"},
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
u := ui.FromContext(cmd.Context())
account, err := requireAccount(flags)
if err != nil {
return err
}
tasklistID := strings.TrimSpace(args[0])
if tasklistID == "" {
return usage("empty tasklistId")
}
if strings.TrimSpace(title) == "" {
return usage("required: --title")
}
svc, err := newTasksService(cmd.Context(), account)
if err != nil {
return err
}
task := &tasks.Task{
Title: strings.TrimSpace(title),
Notes: strings.TrimSpace(notes),
Due: strings.TrimSpace(due),
}
call := svc.Tasks.Insert(tasklistID, task)
if strings.TrimSpace(parent) != "" {
call = call.Parent(strings.TrimSpace(parent))
}
if strings.TrimSpace(previous) != "" {
call = call.Previous(strings.TrimSpace(previous))
}
created, err := call.Do()
if err != nil {
return err
}
if outfmt.IsJSON(cmd.Context()) {
return outfmt.WriteJSON(os.Stdout, map[string]any{"task": created})
}
u.Out().Printf("id\t%s", created.Id)
u.Out().Printf("title\t%s", created.Title)
if strings.TrimSpace(created.Status) != "" {
u.Out().Printf("status\t%s", created.Status)
}
if strings.TrimSpace(created.Due) != "" {
u.Out().Printf("due\t%s", created.Due)
}
if strings.TrimSpace(created.WebViewLink) != "" {
u.Out().Printf("link\t%s", created.WebViewLink)
}
return nil
},
}
cmd.Flags().StringVar(&title, "title", "", "Task title (required)")
cmd.Flags().StringVar(&notes, "notes", "", "Task notes/description")
cmd.Flags().StringVar(&due, "due", "", "Due date/time (RFC3339)")
cmd.Flags().StringVar(&parent, "parent", "", "Parent task ID (create as subtask)")
cmd.Flags().StringVar(&previous, "previous", "", "Previous sibling task ID (controls ordering)")
return cmd
}
func newTasksUpdateCmd(flags *rootFlags) *cobra.Command {
var title string
var notes string
var due string
var status string
cmd := &cobra.Command{
Use: "update <tasklistId> <taskId>",
Short: "Update a task",
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
u := ui.FromContext(cmd.Context())
account, err := requireAccount(flags)
if err != nil {
return err
}
tasklistID := strings.TrimSpace(args[0])
taskID := strings.TrimSpace(args[1])
if tasklistID == "" {
return usage("empty tasklistId")
}
if taskID == "" {
return usage("empty taskId")
}
patch := &tasks.Task{}
changed := false
if cmd.Flags().Changed("title") {
patch.Title = strings.TrimSpace(title)
changed = true
}
if cmd.Flags().Changed("notes") {
patch.Notes = strings.TrimSpace(notes)
changed = true
}
if cmd.Flags().Changed("due") {
patch.Due = strings.TrimSpace(due)
changed = true
}
if cmd.Flags().Changed("status") {
patch.Status = strings.TrimSpace(status)
changed = true
}
if !changed {
return usage("no fields to update (set at least one of: --title, --notes, --due, --status)")
}
if cmd.Flags().Changed("status") && patch.Status != "" && patch.Status != "needsAction" && patch.Status != "completed" {
return usage("invalid --status (expected needsAction or completed)")
}
svc, err := newTasksService(cmd.Context(), account)
if err != nil {
return err
}
updated, err := svc.Tasks.Patch(tasklistID, taskID, patch).Do()
if err != nil {
return err
}
if outfmt.IsJSON(cmd.Context()) {
return outfmt.WriteJSON(os.Stdout, map[string]any{"task": updated})
}
u.Out().Printf("id\t%s", updated.Id)
u.Out().Printf("title\t%s", updated.Title)
if strings.TrimSpace(updated.Status) != "" {
u.Out().Printf("status\t%s", updated.Status)
}
if strings.TrimSpace(updated.Due) != "" {
u.Out().Printf("due\t%s", updated.Due)
}
if strings.TrimSpace(updated.WebViewLink) != "" {
u.Out().Printf("link\t%s", updated.WebViewLink)
}
return nil
},
}
cmd.Flags().StringVar(&title, "title", "", "New title (set empty to clear)")
cmd.Flags().StringVar(&notes, "notes", "", "New notes (set empty to clear)")
cmd.Flags().StringVar(&due, "due", "", "New due date/time (RFC3339; set empty to clear)")
cmd.Flags().StringVar(&status, "status", "", "New status: needsAction|completed (set empty to clear)")
return cmd
}
func newTasksDoneCmd(flags *rootFlags) *cobra.Command {
cmd := &cobra.Command{
Use: "done <tasklistId> <taskId>",
Short: "Mark a task as completed",
Aliases: []string{"complete"},
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
u := ui.FromContext(cmd.Context())
account, err := requireAccount(flags)
if err != nil {
return err
}
tasklistID := strings.TrimSpace(args[0])
taskID := strings.TrimSpace(args[1])
if tasklistID == "" {
return usage("empty tasklistId")
}
if taskID == "" {
return usage("empty taskId")
}
svc, err := newTasksService(cmd.Context(), account)
if err != nil {
return err
}
updated, err := svc.Tasks.Patch(tasklistID, taskID, &tasks.Task{Status: "completed"}).Do()
if err != nil {
return err
}
if outfmt.IsJSON(cmd.Context()) {
return outfmt.WriteJSON(os.Stdout, map[string]any{"task": updated})
}
u.Out().Printf("id\t%s", updated.Id)
u.Out().Printf("status\t%s", strings.TrimSpace(updated.Status))
return nil
},
}
return cmd
}
func newTasksUndoCmd(flags *rootFlags) *cobra.Command {
cmd := &cobra.Command{
Use: "undo <tasklistId> <taskId>",
Short: "Mark a task as not completed",
Aliases: []string{"uncomplete", "undone"},
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
u := ui.FromContext(cmd.Context())
account, err := requireAccount(flags)
if err != nil {
return err
}
tasklistID := strings.TrimSpace(args[0])
taskID := strings.TrimSpace(args[1])
if tasklistID == "" {
return usage("empty tasklistId")
}
if taskID == "" {
return usage("empty taskId")
}
svc, err := newTasksService(cmd.Context(), account)
if err != nil {
return err
}
updated, err := svc.Tasks.Patch(tasklistID, taskID, &tasks.Task{Status: "needsAction"}).Do()
if err != nil {
return err
}
if outfmt.IsJSON(cmd.Context()) {
return outfmt.WriteJSON(os.Stdout, map[string]any{"task": updated})
}
u.Out().Printf("id\t%s", updated.Id)
u.Out().Printf("status\t%s", strings.TrimSpace(updated.Status))
return nil
},
}
return cmd
}
func newTasksDeleteCmd(flags *rootFlags) *cobra.Command {
cmd := &cobra.Command{
Use: "delete <tasklistId> <taskId>",
Short: "Delete a task",
Aliases: []string{"rm", "del"},
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
u := ui.FromContext(cmd.Context())
account, err := requireAccount(flags)
if err != nil {
return err
}
tasklistID := strings.TrimSpace(args[0])
taskID := strings.TrimSpace(args[1])
if tasklistID == "" {
return usage("empty tasklistId")
}
if taskID == "" {
return usage("empty taskId")
}
if confirmErr := confirmDestructive(cmd, flags, fmt.Sprintf("delete task %s from list %s", taskID, tasklistID)); confirmErr != nil {
return confirmErr
}
svc, err := newTasksService(cmd.Context(), account)
if err != nil {
return err
}
if err := svc.Tasks.Delete(tasklistID, taskID).Do(); err != nil {
return err
}
if outfmt.IsJSON(cmd.Context()) {
return outfmt.WriteJSON(os.Stdout, map[string]any{
"deleted": true,
"id": taskID,
})
}
u.Out().Printf("deleted\ttrue")
u.Out().Printf("id\t%s", taskID)
return nil
},
}
return cmd
}
func newTasksClearCmd(flags *rootFlags) *cobra.Command {
cmd := &cobra.Command{
Use: "clear <tasklistId>",
Short: "Clear completed tasks from a task list",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
u := ui.FromContext(cmd.Context())
account, err := requireAccount(flags)
if err != nil {
return err
}
tasklistID := strings.TrimSpace(args[0])
if tasklistID == "" {
return usage("empty tasklistId")
}
if confirmErr := confirmDestructive(cmd, flags, fmt.Sprintf("clear completed tasks from list %s", tasklistID)); confirmErr != nil {
return confirmErr
}
svc, err := newTasksService(cmd.Context(), account)
if err != nil {
return err
}
if err := svc.Tasks.Clear(tasklistID).Do(); err != nil {
return err
}
if outfmt.IsJSON(cmd.Context()) {
return outfmt.WriteJSON(os.Stdout, map[string]any{
"cleared": true,
"tasklistId": tasklistID,
})
}
u.Out().Printf("cleared\ttrue")
u.Out().Printf("tasklistId\t%s", tasklistID)
return nil
},
}
return cmd
}