@@ -0,0 +1,27 @@
+// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+// Package area provides commands for viewing configured areas.
+package area
+
+import (
+ "errors"
+
+ "github.com/spf13/cobra"
+)
+
+// ErrUnknownArea indicates the specified area key was not found in config.
+var ErrUnknownArea = errors.New("unknown area key")
+
+// Cmd is the parent command for area operations.
+var Cmd = &cobra.Command{
+ Use: "area",
+ Short: "View configured areas",
+ GroupID: "resources",
+}
+
+func init() {
+ Cmd.AddCommand(ListCmd)
+ Cmd.AddCommand(ShowCmd)
+}
@@ -0,0 +1,120 @@
+// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+package area
+
+import (
+ "encoding/json"
+ "fmt"
+
+ "git.secluded.site/lune/internal/config"
+ "git.secluded.site/lune/internal/ui"
+ "github.com/charmbracelet/lipgloss"
+ "github.com/charmbracelet/lipgloss/table"
+ "github.com/spf13/cobra"
+)
+
+// ListCmd lists configured areas and their goals.
+var ListCmd = &cobra.Command{
+ Use: "list",
+ Short: "List configured areas",
+ Long: `List areas configured in lune.
+
+Areas are copied from the Lunatask desktop app during 'lune init'.
+Each area may have goals associated with it.`,
+ RunE: runList,
+}
+
+func init() {
+ ListCmd.Flags().Bool("json", false, "Output as JSON")
+}
+
+func runList(cmd *cobra.Command, _ []string) error {
+ cfg, err := config.Load()
+ if err != nil {
+ fmt.Fprintln(cmd.ErrOrStderr(), ui.Error.Render("Config not found; run 'lune init' to configure areas"))
+
+ return err
+ }
+
+ if len(cfg.Areas) == 0 {
+ fmt.Fprintln(cmd.OutOrStdout(), "No areas configured")
+
+ return nil
+ }
+
+ jsonFlag, _ := cmd.Flags().GetBool("json")
+ if jsonFlag {
+ return outputJSON(cmd, cfg.Areas)
+ }
+
+ return outputTable(cmd, cfg.Areas)
+}
+
+func outputJSON(cmd *cobra.Command, areas []config.Area) error {
+ enc := json.NewEncoder(cmd.OutOrStdout())
+ enc.SetIndent("", " ")
+
+ if err := enc.Encode(areas); err != nil {
+ return fmt.Errorf("encoding JSON: %w", err)
+ }
+
+ return nil
+}
+
+func outputTable(cmd *cobra.Command, areas []config.Area) error {
+ rows := make([][]string, 0, len(areas))
+
+ for _, area := range areas {
+ goals := formatGoals(area.Goals)
+ rows = append(rows, []string{area.Key, area.Name, goals})
+ }
+
+ tbl := table.New().
+ Headers("KEY", "NAME", "GOALS").
+ Rows(rows...).
+ StyleFunc(func(row, col int) lipgloss.Style {
+ if row == table.HeaderRow {
+ return ui.Bold
+ }
+
+ return lipgloss.NewStyle()
+ }).
+ Border(lipgloss.HiddenBorder())
+
+ fmt.Fprintln(cmd.OutOrStdout(), tbl.Render())
+
+ return nil
+}
+
+func formatGoals(goals []config.Goal) string {
+ if len(goals) == 0 {
+ return "-"
+ }
+
+ keys := make([]string, len(goals))
+ for i, g := range goals {
+ keys[i] = g.Key
+ }
+
+ if len(keys) <= 3 {
+ return joinKeys(keys)
+ }
+
+ return joinKeys(keys[:3]) + fmt.Sprintf(" (+%d)", len(keys)-3)
+}
+
+func joinKeys(keys []string) string {
+ result := ""
+
+ for i, k := range keys {
+ if i > 0 {
+ result += ", "
+ }
+
+ result += k
+ }
+
+ return result
+}
@@ -0,0 +1,102 @@
+// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+package area
+
+import (
+ "fmt"
+
+ "git.secluded.site/lune/internal/client"
+ "git.secluded.site/lune/internal/config"
+ "git.secluded.site/lune/internal/deeplink"
+ "git.secluded.site/lune/internal/stats"
+ "git.secluded.site/lune/internal/ui"
+ "github.com/spf13/cobra"
+)
+
+// ShowCmd displays details for a specific area.
+var ShowCmd = &cobra.Command{
+ Use: "show KEY",
+ Short: "Show area details",
+ Long: `Show detailed information for a configured area.
+
+Displays the area's name, ID, deeplink, goals, and uncompleted task count.`,
+ Args: cobra.ExactArgs(1),
+ RunE: runShow,
+}
+
+func runShow(cmd *cobra.Command, args []string) error {
+ areaKey := args[0]
+
+ cfg, err := config.Load()
+ if err != nil {
+ 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)
+ }
+
+ apiClient, err := client.New()
+ if err != nil {
+ return err
+ }
+
+ counter := stats.NewTaskCounter(apiClient)
+
+ return printAreaDetails(cmd, area, counter)
+}
+
+func printAreaDetails(cmd *cobra.Command, area *config.Area, counter *stats.TaskCounter) error {
+ link, _ := deeplink.Build(deeplink.Area, area.ID)
+
+ fmt.Fprintf(cmd.OutOrStdout(), "%s (%s)\n", ui.H1.Render(area.Name), area.Key)
+ fmt.Fprintf(cmd.OutOrStdout(), " ID: %s\n", area.ID)
+ fmt.Fprintf(cmd.OutOrStdout(), " Link: %s\n", link)
+
+ taskCount, err := counter.UncompletedInArea(cmd.Context(), area.ID)
+ if err != nil {
+ fmt.Fprintf(cmd.OutOrStdout(), " Tasks: %s\n", ui.Warning.Render("unable to fetch"))
+ } else {
+ fmt.Fprintf(cmd.OutOrStdout(), " Tasks: %d uncompleted\n", taskCount)
+ }
+
+ if len(area.Goals) == 0 {
+ fmt.Fprintln(cmd.OutOrStdout(), "\n No goals configured")
+
+ return nil
+ }
+
+ fmt.Fprintln(cmd.OutOrStdout())
+
+ for _, goal := range area.Goals {
+ if err := printGoalSummary(cmd, &goal, counter); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func printGoalSummary(cmd *cobra.Command, goal *config.Goal, counter *stats.TaskCounter) error {
+ goalLink, _ := deeplink.Build(deeplink.Goal, goal.ID)
+
+ fmt.Fprintf(cmd.OutOrStdout(), " %s (%s)\n", ui.H2.Render(goal.Name), goal.Key)
+ fmt.Fprintf(cmd.OutOrStdout(), " ID: %s\n", goal.ID)
+ fmt.Fprintf(cmd.OutOrStdout(), " Link: %s\n", goalLink)
+
+ taskCount, err := counter.UncompletedInGoal(cmd.Context(), goal.ID)
+ if err != nil {
+ fmt.Fprintf(cmd.OutOrStdout(), " Tasks: %s\n", ui.Warning.Render("unable to fetch"))
+ } else {
+ fmt.Fprintf(cmd.OutOrStdout(), " Tasks: %d uncompleted\n", taskCount)
+ }
+
+ return nil
+}
@@ -9,6 +9,7 @@ import (
"context"
"os"
+ "git.secluded.site/lune/cmd/area"
"git.secluded.site/lune/cmd/habit"
initcmd "git.secluded.site/lune/cmd/init"
"git.secluded.site/lune/cmd/journal"
@@ -42,6 +43,7 @@ func init() {
rootCmd.AddCommand(initcmd.Cmd)
rootCmd.AddCommand(pingCmd)
+ rootCmd.AddCommand(area.Cmd)
rootCmd.AddCommand(task.Cmd)
rootCmd.AddCommand(note.Cmd)
rootCmd.AddCommand(person.Cmd)