@@ -5,34 +5,206 @@
package task
import (
+ "encoding/json"
+ "errors"
"fmt"
+ "text/tabwriter"
+ "time"
+ "git.secluded.site/go-lunatask"
+ "git.secluded.site/lune/internal/client"
"git.secluded.site/lune/internal/completion"
+ "git.secluded.site/lune/internal/config"
+ "git.secluded.site/lune/internal/ui"
"github.com/spf13/cobra"
)
+// ErrUnknownArea indicates the specified area key was not found in config.
+var ErrUnknownArea = errors.New("unknown area key")
+
// ListCmd lists tasks. Exported for potential use by shortcuts.
var ListCmd = &cobra.Command{
Use: "list",
Short: "List tasks",
Long: `List tasks from Lunatask.
+By default, shows incomplete tasks and tasks completed today.
+Use --all to show entire task history.
+
Note: Due to end-to-end encryption, task names and notes
are not available through the API. Only metadata is shown.`,
- RunE: func(cmd *cobra.Command, _ []string) error {
- // TODO: implement task listing
- fmt.Fprintln(cmd.OutOrStdout(), "Task listing not yet implemented")
-
- return nil
- },
+ RunE: runList,
}
func init() {
ListCmd.Flags().StringP("area", "a", "", "Filter by area key")
ListCmd.Flags().StringP("status", "s", "", "Filter by status")
+ ListCmd.Flags().Bool("all", false, "Show all tasks including completed")
ListCmd.Flags().Bool("json", false, "Output as JSON")
_ = ListCmd.RegisterFlagCompletionFunc("area", completion.Areas)
_ = ListCmd.RegisterFlagCompletionFunc("status",
completion.Static("later", "next", "started", "waiting", "completed"))
}
+
+func runList(cmd *cobra.Command, _ []string) error {
+ apiClient, err := client.New()
+ if err != nil {
+ return err
+ }
+
+ tasks, err := apiClient.ListTasks(cmd.Context(), nil)
+ if err != nil {
+ fmt.Fprintln(cmd.ErrOrStderr(), ui.Error.Render("Failed to list tasks"))
+
+ return err
+ }
+
+ areaID, err := resolveAreaFilter(cmd)
+ if err != nil {
+ return err
+ }
+
+ statusFilter := mustGetStringFlag(cmd, "status")
+ showAll := mustGetBoolFlag(cmd, "all")
+ tasks = applyFilters(tasks, areaID, statusFilter, showAll)
+
+ if len(tasks) == 0 {
+ fmt.Fprintln(cmd.OutOrStdout(), ui.Muted.Render("No tasks found"))
+
+ return nil
+ }
+
+ if mustGetBoolFlag(cmd, "json") {
+ return outputJSON(cmd, tasks)
+ }
+
+ return outputTable(cmd, tasks)
+}
+
+// mustGetStringFlag returns the string flag value. Panics if flag doesn't exist
+// (indicates a programming error—flags are defined in init).
+func mustGetStringFlag(cmd *cobra.Command, name string) string {
+ f := cmd.Flags().Lookup(name)
+ if f == nil {
+ panic("flag not defined: " + name)
+ }
+
+ return f.Value.String()
+}
+
+// mustGetBoolFlag returns the bool flag value. Panics if flag doesn't exist.
+func mustGetBoolFlag(cmd *cobra.Command, name string) bool {
+ f := cmd.Flags().Lookup(name)
+ if f == nil {
+ panic("flag not defined: " + name)
+ }
+
+ return f.Value.String() == "true"
+}
+
+func resolveAreaFilter(cmd *cobra.Command) (string, error) {
+ areaKey := mustGetStringFlag(cmd, "area")
+ if areaKey == "" {
+ return "", nil
+ }
+
+ cfg, err := config.Load()
+ if err != nil {
+ if errors.Is(err, config.ErrNotFound) {
+ fmt.Fprintln(cmd.ErrOrStderr(), ui.Error.Render("Config not found; run 'lune init' to configure areas"))
+ }
+
+ return "", err
+ }
+
+ area := cfg.AreaByKey(areaKey)
+ if area == nil {
+ fmt.Fprintln(cmd.ErrOrStderr(), ui.Error.Render("Unknown area: "+areaKey))
+
+ return "", fmt.Errorf("%w: %s", ErrUnknownArea, areaKey)
+ }
+
+ return area.ID, nil
+}
+
+func applyFilters(tasks []lunatask.Task, areaID, statusFilter string, showAll bool) []lunatask.Task {
+ filtered := make([]lunatask.Task, 0, len(tasks))
+ today := time.Now().Truncate(24 * time.Hour)
+
+ for _, task := range tasks {
+ if !matchesFilters(task, areaID, statusFilter, showAll, today) {
+ continue
+ }
+
+ filtered = append(filtered, task)
+ }
+
+ return filtered
+}
+
+func matchesFilters(task lunatask.Task, areaID, statusFilter string, showAll bool, today time.Time) bool {
+ if areaID != "" && (task.AreaID == nil || *task.AreaID != areaID) {
+ return false
+ }
+
+ if statusFilter != "" && (task.Status == nil || string(*task.Status) != statusFilter) {
+ return false
+ }
+
+ // Default filter: exclude completed tasks unless completed today
+ if !showAll && statusFilter == "" && isOldCompleted(task, today) {
+ return false
+ }
+
+ return true
+}
+
+func isOldCompleted(task lunatask.Task, today time.Time) bool {
+ if task.Status == nil || *task.Status != lunatask.StatusCompleted {
+ return false
+ }
+
+ return task.CompletedAt == nil || task.CompletedAt.Before(today)
+}
+
+func outputJSON(cmd *cobra.Command, tasks []lunatask.Task) error {
+ enc := json.NewEncoder(cmd.OutOrStdout())
+ enc.SetIndent("", " ")
+
+ if err := enc.Encode(tasks); err != nil {
+ return fmt.Errorf("encoding JSON: %w", err)
+ }
+
+ return nil
+}
+
+func outputTable(cmd *cobra.Command, tasks []lunatask.Task) error {
+ writer := tabwriter.NewWriter(cmd.OutOrStdout(), 0, 0, 2, ' ', 0)
+
+ header := ui.Bold.Render("ID") + "\t" + ui.Bold.Render("STATUS") + "\t" +
+ ui.Bold.Render("SCHEDULED") + "\t" + ui.Bold.Render("CREATED")
+ fmt.Fprintln(writer, header)
+
+ for _, task := range tasks {
+ status := "-"
+ if task.Status != nil {
+ status = string(*task.Status)
+ }
+
+ scheduled := "-"
+ if task.ScheduledOn != nil {
+ scheduled = task.ScheduledOn.String()
+ }
+
+ created := ui.FormatDate(task.CreatedAt)
+
+ fmt.Fprintf(writer, "%s\t%s\t%s\t%s\n", task.ID, status, scheduled, created)
+ }
+
+ if err := writer.Flush(); err != nil {
+ return fmt.Errorf("writing output: %w", err)
+ }
+
+ return nil
+}