From bc97d8435e2e49a09530cb865209ea75745a2027 Mon Sep 17 00:00:00 2001 From: Amolith Date: Sun, 21 Dec 2025 19:01:23 -0700 Subject: [PATCH] feat(task): implement show command Displays task metadata including status, area/goal (resolved to names from config when available), scheduling dates, and attributes like priority, estimate, progress, motivation, and eisenhower quadrant. Adds AreaByID and GoalByID helpers to config package for name resolution. Upgrades go-lunatask to v0.1.0-rc9 for Priority type support. Assisted-by: Claude Sonnet 4 via Crush --- cmd/task/add.go | 2 +- cmd/task/show.go | 153 +++++++++++++++++++++++++++++++++++--- go.mod | 2 +- go.sum | 4 +- internal/config/config.go | 29 ++++++++ 5 files changed, 176 insertions(+), 14 deletions(-) diff --git a/cmd/task/add.go b/cmd/task/add.go index e3981d0136c998b2064df447c81aaf1439da87a9..fc104fdf10aeac137575166b86ff749f45b09f78 100644 --- a/cmd/task/add.go +++ b/cmd/task/add.go @@ -172,7 +172,7 @@ func applyOptionalFlags(cmd *cobra.Command, builder *lunatask.TaskBuilder) error } if priority, _ := cmd.Flags().GetInt("priority"); priority != 0 { - builder.WithPriority(priority) + builder.Priority(lunatask.Priority(priority)) } if estimate, _ := cmd.Flags().GetInt("estimate"); estimate != 0 { diff --git a/cmd/task/show.go b/cmd/task/show.go index 781465809a018d09aa4f505c51364722ca81748f..f8f3f46c06bfb3ecd7b3a0ce1907ddfd07116802 100644 --- a/cmd/task/show.go +++ b/cmd/task/show.go @@ -5,8 +5,14 @@ package task import ( + "encoding/json" "fmt" + "git.secluded.site/go-lunatask" + "git.secluded.site/lune/internal/client" + "git.secluded.site/lune/internal/config" + "git.secluded.site/lune/internal/deeplink" + "git.secluded.site/lune/internal/ui" "git.secluded.site/lune/internal/validate" "github.com/spf13/cobra" ) @@ -15,20 +21,147 @@ import ( var ShowCmd = &cobra.Command{ Use: "show ID", Short: "Show task details", - Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - id, err := validate.Reference(args[0]) - if err != nil { - return err - } + Long: `Show detailed information for a task. - // TODO: implement task show - fmt.Fprintf(cmd.OutOrStdout(), "Showing task %s (not yet implemented)\n", id) +Accepts a UUID or lunatask:// deep link. - return nil - }, +Note: Due to end-to-end encryption, task name and notes +are not available through the API. Only metadata is shown.`, + Args: cobra.ExactArgs(1), + RunE: runShow, } func init() { ShowCmd.Flags().Bool("json", false, "Output as JSON") } + +func runShow(cmd *cobra.Command, args []string) error { + id, err := validate.Reference(args[0]) + if err != nil { + return err + } + + apiClient, err := client.New() + if err != nil { + return err + } + + task, err := apiClient.GetTask(cmd.Context(), id) + if err != nil { + fmt.Fprintln(cmd.ErrOrStderr(), ui.Error.Render("Failed to get task")) + + return err + } + + if mustGetBoolFlag(cmd, "json") { + return outputTaskJSON(cmd, task) + } + + return printTaskDetails(cmd, task) +} + +func outputTaskJSON(cmd *cobra.Command, task *lunatask.Task) error { + enc := json.NewEncoder(cmd.OutOrStdout()) + enc.SetIndent("", " ") + + if err := enc.Encode(task); err != nil { + return fmt.Errorf("encoding JSON: %w", err) + } + + return nil +} + +func printTaskDetails(cmd *cobra.Command, task *lunatask.Task) error { + link, _ := deeplink.Build(deeplink.Task, task.ID) + + status := "unknown" + if task.Status != nil { + status = string(*task.Status) + } + + fmt.Fprintf(cmd.OutOrStdout(), "%s\n", ui.H1.Render("Task: "+status)) + fmt.Fprintf(cmd.OutOrStdout(), " ID: %s\n", task.ID) + fmt.Fprintf(cmd.OutOrStdout(), " Link: %s\n", link) + + printAreaGoal(cmd, task) + + if task.ScheduledOn != nil { + fmt.Fprintf(cmd.OutOrStdout(), " Scheduled: %s\n", ui.FormatDate(task.ScheduledOn.Time)) + } + + if task.CompletedAt != nil { + fmt.Fprintf(cmd.OutOrStdout(), " Completed: %s\n", ui.FormatDate(*task.CompletedAt)) + } + + printTaskAttributes(cmd, task) + + fmt.Fprintf(cmd.OutOrStdout(), " Created: %s\n", ui.FormatDate(task.CreatedAt)) + fmt.Fprintf(cmd.OutOrStdout(), " Updated: %s\n", ui.FormatDate(task.UpdatedAt)) + + return nil +} + +func printAreaGoal(cmd *cobra.Command, task *lunatask.Task) { + cfg, _ := config.Load() + + if task.AreaID != nil { + areaDisplay := *task.AreaID + if cfg != nil { + if area := cfg.AreaByID(*task.AreaID); area != nil { + areaDisplay = fmt.Sprintf("%s (%s)", area.Name, area.Key) + } + } + + fmt.Fprintf(cmd.OutOrStdout(), " Area: %s\n", areaDisplay) + } + + if task.GoalID != nil { + goalDisplay := *task.GoalID + if cfg != nil { + if match := cfg.GoalByID(*task.GoalID); match != nil { + goalDisplay = fmt.Sprintf("%s (%s)", match.Goal.Name, match.Goal.Key) + } + } + + fmt.Fprintf(cmd.OutOrStdout(), " Goal: %s\n", goalDisplay) + } +} + +func printTaskAttributes(cmd *cobra.Command, task *lunatask.Task) { + if task.Estimate != nil { + fmt.Fprintf(cmd.OutOrStdout(), " Estimate: %d min\n", *task.Estimate) + } + + if task.Priority != nil && *task.Priority != lunatask.PriorityNormal { + fmt.Fprintf(cmd.OutOrStdout(), " Priority: %s\n", task.Priority) + } + + if task.Progress != nil { + fmt.Fprintf(cmd.OutOrStdout(), " Progress: %d%%\n", *task.Progress) + } + + if task.Motivation != nil && *task.Motivation != lunatask.MotivationUnknown { + fmt.Fprintf(cmd.OutOrStdout(), " Motivation: %s\n", *task.Motivation) + } + + if task.Eisenhower != nil && *task.Eisenhower != lunatask.EisenhowerUncategorized { + fmt.Fprintf(cmd.OutOrStdout(), " Eisenhower: %s\n", formatEisenhower(*task.Eisenhower)) + } +} + +func formatEisenhower(e lunatask.Eisenhower) string { + switch e { + case lunatask.EisenhowerUncategorized: + return "uncategorized" + case lunatask.EisenhowerDoNow: + return "do now (important + urgent)" + case lunatask.EisenhowerDoLater: + return "do later (important)" + case lunatask.EisenhowerDelegate: + return "delegate (urgent)" + case lunatask.EisenhowerEliminate: + return "eliminate" + default: + return "unknown" + } +} diff --git a/go.mod b/go.mod index fd386f84583dae82ddcbf33f5c90ad4177f007ce..9d59632f4271ace6d5a9589fa02cd31efb97a249 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module git.secluded.site/lune go 1.25.5 require ( - git.secluded.site/go-lunatask v0.1.0-rc8 + git.secluded.site/go-lunatask v0.1.0-rc9 github.com/BurntSushi/toml v1.6.0 github.com/charmbracelet/fang v0.4.4 github.com/charmbracelet/huh v0.8.0 diff --git a/go.sum b/go.sum index ace299c61faa045b7e4340246291517924846b36..3d9e80eb5d49a4374737b40e462be67e0fd4f559 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,8 @@ al.essio.dev/pkg/shellescape v1.5.1 h1:86HrALUujYS/h+GtqoB26SBEdkWfmMI6FubjXlsXy al.essio.dev/pkg/shellescape v1.5.1/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890= charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251106193318-19329a3e8410 h1:D9PbaszZYpB4nj+d6HTWr1onlmlyuGVNfL9gAi8iB3k= charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251106193318-19329a3e8410/go.mod h1:1qZyvvVCenJO2M1ac2mX0yyiIZJoZmDM4DG4s0udJkU= -git.secluded.site/go-lunatask v0.1.0-rc8 h1:gfL5VII4KTbv83P87VUhPcN5gmUR7a6XKkokBSgDGRs= -git.secluded.site/go-lunatask v0.1.0-rc8/go.mod h1:sWUQxme1z7qfsfS59nU5hqPvsRCt+HBmT/yBeIn6Fmc= +git.secluded.site/go-lunatask v0.1.0-rc9 h1:ri8PDl7Xzg3mGStvHBxvL5PKOlBSZGxKDBzkqurLpEw= +git.secluded.site/go-lunatask v0.1.0-rc9/go.mod h1:sWUQxme1z7qfsfS59nU5hqPvsRCt+HBmT/yBeIn6Fmc= github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= diff --git a/internal/config/config.go b/internal/config/config.go index a51c4ad7a240f52495b1093bf03d003a42b03d99..41a794f47ab7d07ac36be931938c8886597794b4 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -194,3 +194,32 @@ func (c *Config) FindGoalsByKey(key string) []GoalMatch { return matches } + +// AreaByID finds an area by its ID. +func (c *Config) AreaByID(id string) *Area { + for i := range c.Areas { + if c.Areas[i].ID == id { + return &c.Areas[i] + } + } + + return nil +} + +// GoalByID finds a goal by its ID across all areas. +func (c *Config) GoalByID(id string) *GoalMatch { + for i := range c.Areas { + area := &c.Areas[i] + + for j := range area.Goals { + if area.Goals[j].ID == id { + return &GoalMatch{ + Goal: &area.Goals[j], + Area: area, + } + } + } + } + + return nil +}