1// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
2//
3// SPDX-License-Identifier: AGPL-3.0-or-later
4
5package task
6
7import (
8 "encoding/json"
9 "errors"
10 "fmt"
11 "text/tabwriter"
12 "time"
13
14 "git.secluded.site/go-lunatask"
15 "git.secluded.site/lune/internal/client"
16 "git.secluded.site/lune/internal/completion"
17 "git.secluded.site/lune/internal/config"
18 "git.secluded.site/lune/internal/ui"
19 "github.com/spf13/cobra"
20)
21
22// ErrUnknownArea indicates the specified area key was not found in config.
23var ErrUnknownArea = errors.New("unknown area key")
24
25// ListCmd lists tasks. Exported for potential use by shortcuts.
26var ListCmd = &cobra.Command{
27 Use: "list",
28 Short: "List tasks",
29 Long: `List tasks from Lunatask.
30
31By default, shows incomplete tasks and tasks completed today.
32Use --all to show entire task history.
33
34Note: Due to end-to-end encryption, task names and notes
35are not available through the API. Only metadata is shown.`,
36 RunE: runList,
37}
38
39func init() {
40 ListCmd.Flags().StringP("area", "a", "", "Filter by area key")
41 ListCmd.Flags().StringP("status", "s", "", "Filter by status")
42 ListCmd.Flags().Bool("all", false, "Show all tasks including completed")
43 ListCmd.Flags().Bool("json", false, "Output as JSON")
44
45 _ = ListCmd.RegisterFlagCompletionFunc("area", completion.Areas)
46 _ = ListCmd.RegisterFlagCompletionFunc("status",
47 completion.Static("later", "next", "started", "waiting", "completed"))
48}
49
50func runList(cmd *cobra.Command, _ []string) error {
51 apiClient, err := client.New()
52 if err != nil {
53 return err
54 }
55
56 tasks, err := apiClient.ListTasks(cmd.Context(), nil)
57 if err != nil {
58 fmt.Fprintln(cmd.ErrOrStderr(), ui.Error.Render("Failed to list tasks"))
59
60 return err
61 }
62
63 areaID, err := resolveAreaFilter(cmd)
64 if err != nil {
65 return err
66 }
67
68 statusFilter := mustGetStringFlag(cmd, "status")
69 showAll := mustGetBoolFlag(cmd, "all")
70 tasks = applyFilters(tasks, areaID, statusFilter, showAll)
71
72 if len(tasks) == 0 {
73 fmt.Fprintln(cmd.OutOrStdout(), ui.Muted.Render("No tasks found"))
74
75 return nil
76 }
77
78 if mustGetBoolFlag(cmd, "json") {
79 return outputJSON(cmd, tasks)
80 }
81
82 return outputTable(cmd, tasks)
83}
84
85// mustGetStringFlag returns the string flag value. Panics if flag doesn't exist
86// (indicates a programming errorāflags are defined in init).
87func mustGetStringFlag(cmd *cobra.Command, name string) string {
88 f := cmd.Flags().Lookup(name)
89 if f == nil {
90 panic("flag not defined: " + name)
91 }
92
93 return f.Value.String()
94}
95
96// mustGetBoolFlag returns the bool flag value. Panics if flag doesn't exist.
97func mustGetBoolFlag(cmd *cobra.Command, name string) bool {
98 f := cmd.Flags().Lookup(name)
99 if f == nil {
100 panic("flag not defined: " + name)
101 }
102
103 return f.Value.String() == "true"
104}
105
106func resolveAreaFilter(cmd *cobra.Command) (string, error) {
107 areaKey := mustGetStringFlag(cmd, "area")
108 if areaKey == "" {
109 return "", nil
110 }
111
112 cfg, err := config.Load()
113 if err != nil {
114 if errors.Is(err, config.ErrNotFound) {
115 fmt.Fprintln(cmd.ErrOrStderr(), ui.Error.Render("Config not found; run 'lune init' to configure areas"))
116 }
117
118 return "", err
119 }
120
121 area := cfg.AreaByKey(areaKey)
122 if area == nil {
123 fmt.Fprintln(cmd.ErrOrStderr(), ui.Error.Render("Unknown area: "+areaKey))
124
125 return "", fmt.Errorf("%w: %s", ErrUnknownArea, areaKey)
126 }
127
128 return area.ID, nil
129}
130
131func applyFilters(tasks []lunatask.Task, areaID, statusFilter string, showAll bool) []lunatask.Task {
132 filtered := make([]lunatask.Task, 0, len(tasks))
133 today := time.Now().Truncate(24 * time.Hour)
134
135 for _, task := range tasks {
136 if !matchesFilters(task, areaID, statusFilter, showAll, today) {
137 continue
138 }
139
140 filtered = append(filtered, task)
141 }
142
143 return filtered
144}
145
146func matchesFilters(task lunatask.Task, areaID, statusFilter string, showAll bool, today time.Time) bool {
147 if areaID != "" && (task.AreaID == nil || *task.AreaID != areaID) {
148 return false
149 }
150
151 if statusFilter != "" && (task.Status == nil || string(*task.Status) != statusFilter) {
152 return false
153 }
154
155 // Default filter: exclude completed tasks unless completed today
156 if !showAll && statusFilter == "" && isOldCompleted(task, today) {
157 return false
158 }
159
160 return true
161}
162
163func isOldCompleted(task lunatask.Task, today time.Time) bool {
164 if task.Status == nil || *task.Status != lunatask.StatusCompleted {
165 return false
166 }
167
168 return task.CompletedAt == nil || task.CompletedAt.Before(today)
169}
170
171func outputJSON(cmd *cobra.Command, tasks []lunatask.Task) error {
172 enc := json.NewEncoder(cmd.OutOrStdout())
173 enc.SetIndent("", " ")
174
175 if err := enc.Encode(tasks); err != nil {
176 return fmt.Errorf("encoding JSON: %w", err)
177 }
178
179 return nil
180}
181
182func outputTable(cmd *cobra.Command, tasks []lunatask.Task) error {
183 writer := tabwriter.NewWriter(cmd.OutOrStdout(), 0, 0, 2, ' ', 0)
184
185 header := ui.Bold.Render("ID") + "\t" + ui.Bold.Render("STATUS") + "\t" +
186 ui.Bold.Render("SCHEDULED") + "\t" + ui.Bold.Render("CREATED")
187 fmt.Fprintln(writer, header)
188
189 for _, task := range tasks {
190 status := "-"
191 if task.Status != nil {
192 status = string(*task.Status)
193 }
194
195 scheduled := "-"
196 if task.ScheduledOn != nil {
197 scheduled = task.ScheduledOn.String()
198 }
199
200 created := ui.FormatDate(task.CreatedAt)
201
202 fmt.Fprintf(writer, "%s\t%s\t%s\t%s\n", task.ID, status, scheduled, created)
203 }
204
205 if err := writer.Flush(); err != nil {
206 return fmt.Errorf("writing output: %w", err)
207 }
208
209 return nil
210}