1// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
2//
3// SPDX-License-Identifier: AGPL-3.0-or-later
4
5package goal
6
7import (
8 "errors"
9 "fmt"
10 "strings"
11
12 "git.secluded.site/lune/internal/client"
13 "git.secluded.site/lune/internal/completion"
14 "git.secluded.site/lune/internal/config"
15 "git.secluded.site/lune/internal/deeplink"
16 "git.secluded.site/lune/internal/stats"
17 "git.secluded.site/lune/internal/ui"
18 "github.com/spf13/cobra"
19)
20
21// ErrAmbiguousGoal indicates the goal key exists in multiple areas.
22var ErrAmbiguousGoal = errors.New("ambiguous goal key")
23
24// ShowCmd displays details for a specific goal.
25var ShowCmd = &cobra.Command{
26 Use: "show KEY",
27 Short: "Show goal details",
28 Long: `Show detailed information for a configured goal.
29
30If the goal key exists in multiple areas, use --area to disambiguate.
31Displays the goal's name, ID, deeplink, and uncompleted task count.`,
32 Args: cobra.ExactArgs(1),
33 RunE: runShow,
34}
35
36func init() {
37 ShowCmd.Flags().StringP("area", "a", "", "Area key (required if goal key is ambiguous)")
38 _ = ShowCmd.RegisterFlagCompletionFunc("area", completion.Areas)
39}
40
41func runShow(cmd *cobra.Command, args []string) error {
42 goalKey := args[0]
43 areaKey, _ := cmd.Flags().GetString("area")
44
45 cfg, err := config.Load()
46 if err != nil {
47 fmt.Fprintln(cmd.ErrOrStderr(), ui.Error.Render("Config not found; run 'lune init' to configure areas"))
48
49 return err
50 }
51
52 match, err := resolveGoal(cmd, cfg, goalKey, areaKey)
53 if err != nil {
54 return err
55 }
56
57 apiClient, err := client.New()
58 if err != nil {
59 return err
60 }
61
62 counter := stats.NewTaskCounter(apiClient)
63
64 return printGoalDetails(cmd, match.Goal, match.Area, counter)
65}
66
67func resolveGoal(cmd *cobra.Command, cfg *config.Config, goalKey, areaKey string) (*config.GoalMatch, error) {
68 if areaKey != "" {
69 area := cfg.AreaByKey(areaKey)
70 if area == nil {
71 fmt.Fprintln(cmd.ErrOrStderr(), ui.Error.Render("Unknown area: "+areaKey))
72
73 return nil, fmt.Errorf("%w: %s", ErrUnknownArea, areaKey)
74 }
75
76 goal := area.GoalByKey(goalKey)
77 if goal == nil {
78 fmt.Fprintln(cmd.ErrOrStderr(), ui.Error.Render("Unknown goal: "+goalKey))
79
80 return nil, fmt.Errorf("%w: %s", ErrUnknownGoal, goalKey)
81 }
82
83 return &config.GoalMatch{Goal: goal, Area: area}, nil
84 }
85
86 matches := cfg.FindGoalsByKey(goalKey)
87
88 switch len(matches) {
89 case 0:
90 fmt.Fprintln(cmd.ErrOrStderr(), ui.Error.Render("Unknown goal: "+goalKey))
91
92 return nil, fmt.Errorf("%w: %s", ErrUnknownGoal, goalKey)
93
94 case 1:
95 return &matches[0], nil
96
97 default:
98 areas := make([]string, len(matches))
99 for i, m := range matches {
100 areas[i] = m.Area.Key
101 }
102
103 fmt.Fprintf(cmd.ErrOrStderr(), "%s\n",
104 ui.Error.Render("Goal '"+goalKey+"' exists in multiple areas: "+strings.Join(areas, ", ")))
105 fmt.Fprintln(cmd.ErrOrStderr(), "Use --area to specify which one")
106
107 return nil, fmt.Errorf("%w: %s exists in %s", ErrAmbiguousGoal, goalKey, strings.Join(areas, ", "))
108 }
109}
110
111func printGoalDetails(cmd *cobra.Command, goal *config.Goal, area *config.Area, counter *stats.TaskCounter) error {
112 link, _ := deeplink.Build(deeplink.Goal, goal.ID)
113
114 fmt.Fprintf(cmd.OutOrStdout(), "%s (%s)\n", ui.H1.Render(goal.Name), goal.Key)
115 fmt.Fprintf(cmd.OutOrStdout(), " ID: %s\n", goal.ID)
116 fmt.Fprintf(cmd.OutOrStdout(), " Area: %s (%s)\n", area.Name, area.Key)
117 fmt.Fprintf(cmd.OutOrStdout(), " Link: %s\n", link)
118
119 taskCount, err := counter.UncompletedInGoal(cmd.Context(), goal.ID)
120 if err != nil {
121 fmt.Fprintf(cmd.OutOrStdout(), " Tasks: %s\n", ui.Warning.Render("unable to fetch"))
122 } else {
123 fmt.Fprintf(cmd.OutOrStdout(), " Tasks: %d uncompleted\n", taskCount)
124 }
125
126 return nil
127}