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 "time"
12
13 "git.secluded.site/go-lunatask"
14 "git.secluded.site/lune/internal/client"
15 "git.secluded.site/lune/internal/completion"
16 "git.secluded.site/lune/internal/config"
17 "git.secluded.site/lune/internal/ui"
18 "git.secluded.site/lune/internal/validate"
19 "github.com/charmbracelet/lipgloss"
20 "github.com/charmbracelet/lipgloss/table"
21 "github.com/spf13/cobra"
22)
23
24// ErrUnknownArea indicates the specified area key was not found in config.
25var ErrUnknownArea = errors.New("unknown area key")
26
27// ListCmd lists tasks. Exported for potential use by shortcuts.
28var ListCmd = &cobra.Command{
29 Use: "list",
30 Short: "List tasks",
31 Long: `List tasks from Lunatask.
32
33By default, shows incomplete tasks and tasks completed today.
34Use --all to show entire task history.
35
36Note: Due to end-to-end encryption, task names and notes
37are not available through the API. Only metadata is shown.`,
38 RunE: runList,
39}
40
41func init() {
42 ListCmd.Flags().StringP("area", "a", "", "Filter by area key")
43 ListCmd.Flags().StringP("status", "s", "", "Filter by status")
44 ListCmd.Flags().Bool("all", false, "Show all tasks including completed")
45 ListCmd.Flags().Bool("json", false, "Output as JSON")
46
47 _ = ListCmd.RegisterFlagCompletionFunc("area", completion.Areas)
48 _ = ListCmd.RegisterFlagCompletionFunc("status", completion.TaskStatuses)
49}
50
51func runList(cmd *cobra.Command, _ []string) error {
52 apiClient, err := client.New()
53 if err != nil {
54 return err
55 }
56
57 tasks, err := ui.Spin("Fetching tasksā¦", func() ([]lunatask.Task, error) {
58 return apiClient.ListTasks(cmd.Context(), nil)
59 })
60 if err != nil {
61 return err
62 }
63
64 areaID, err := resolveAreaFilter(cmd)
65 if err != nil {
66 return err
67 }
68
69 statusFilter, err := resolveStatusFilter(cmd)
70 if err != nil {
71 return err
72 }
73
74 showAll := mustGetBoolFlag(cmd, "all")
75 tasks = applyFilters(tasks, areaID, statusFilter, showAll)
76
77 if len(tasks) == 0 {
78 fmt.Fprintln(cmd.OutOrStdout(), "No tasks found")
79
80 return nil
81 }
82
83 if mustGetBoolFlag(cmd, "json") {
84 return outputJSON(cmd, tasks)
85 }
86
87 return outputTable(cmd, tasks)
88}
89
90// mustGetStringFlag returns the string flag value. Panics if flag doesn't exist
91// (indicates a programming errorāflags are defined in init).
92func mustGetStringFlag(cmd *cobra.Command, name string) string {
93 f := cmd.Flags().Lookup(name)
94 if f == nil {
95 panic("flag not defined: " + name)
96 }
97
98 return f.Value.String()
99}
100
101// mustGetBoolFlag returns the bool flag value. Panics if flag doesn't exist.
102func mustGetBoolFlag(cmd *cobra.Command, name string) bool {
103 f := cmd.Flags().Lookup(name)
104 if f == nil {
105 panic("flag not defined: " + name)
106 }
107
108 return f.Value.String() == "true"
109}
110
111func resolveAreaFilter(cmd *cobra.Command) (string, error) {
112 areaKey := mustGetStringFlag(cmd, "area")
113
114 cfg, err := config.Load()
115 if err != nil {
116 // Config not required if no area flag and we just skip default
117 if errors.Is(err, config.ErrNotFound) {
118 if areaKey != "" {
119 return "", err
120 }
121
122 return "", nil
123 }
124
125 return "", err
126 }
127
128 // Use default area if no explicit flag
129 if areaKey == "" {
130 areaKey = cfg.Defaults.Area
131 }
132
133 if areaKey == "" {
134 return "", nil
135 }
136
137 area := cfg.AreaByKey(areaKey)
138 if area == nil {
139 return "", fmt.Errorf("%w: %s", ErrUnknownArea, areaKey)
140 }
141
142 return area.ID, nil
143}
144
145func resolveStatusFilter(cmd *cobra.Command) (string, error) {
146 status := mustGetStringFlag(cmd, "status")
147 if status == "" {
148 return "", nil
149 }
150
151 s, err := validate.TaskStatus(status)
152 if err != nil {
153 return "", err
154 }
155
156 return string(s), nil
157}
158
159func applyFilters(tasks []lunatask.Task, areaID, statusFilter string, showAll bool) []lunatask.Task {
160 opts := &lunatask.TaskFilterOptions{
161 IncludeCompleted: showAll,
162 Today: time.Now(),
163 }
164
165 if areaID != "" {
166 opts.AreaID = &areaID
167 }
168
169 if statusFilter != "" {
170 s := lunatask.TaskStatus(statusFilter)
171 opts.Status = &s
172 }
173
174 return lunatask.FilterTasks(tasks, opts)
175}
176
177func outputJSON(cmd *cobra.Command, tasks []lunatask.Task) error {
178 enc := json.NewEncoder(cmd.OutOrStdout())
179 enc.SetIndent("", " ")
180
181 if err := enc.Encode(tasks); err != nil {
182 return fmt.Errorf("encoding JSON: %w", err)
183 }
184
185 return nil
186}
187
188func outputTable(cmd *cobra.Command, tasks []lunatask.Task) error {
189 rows := make([][]string, 0, len(tasks))
190
191 for _, task := range tasks {
192 status := "-"
193 if task.Status != nil {
194 status = string(*task.Status)
195 }
196
197 scheduled := "-"
198 if task.ScheduledOn != nil {
199 scheduled = ui.FormatDate(task.ScheduledOn.Time)
200 }
201
202 created := ui.FormatDate(task.CreatedAt)
203
204 rows = append(rows, []string{task.ID, status, scheduled, created})
205 }
206
207 tbl := table.New().
208 Headers("ID", "STATUS", "SCHEDULED", "CREATED").
209 Rows(rows...).
210 StyleFunc(func(row, col int) lipgloss.Style {
211 if row == table.HeaderRow {
212 return ui.TableHeaderStyle()
213 }
214
215 return lipgloss.NewStyle()
216 }).
217 Border(ui.TableBorder())
218
219 fmt.Fprintln(cmd.OutOrStdout(), tbl.Render())
220
221 return nil
222}