show.go

  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}