init.go

  1// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
  2//
  3// SPDX-License-Identifier: AGPL-3.0-or-later
  4
  5// Package init provides the interactive setup wizard for lune.
  6package init
  7
  8import (
  9	"errors"
 10	"fmt"
 11
 12	"github.com/charmbracelet/huh"
 13	"github.com/spf13/cobra"
 14
 15	"git.secluded.site/lune/internal/config"
 16	"git.secluded.site/lune/internal/ui"
 17)
 18
 19// Cmd is the init command for interactive setup.
 20var Cmd = &cobra.Command{
 21	Use:   "init",
 22	Short: "Interactive setup wizard",
 23	Long: `Configure lune interactively.
 24
 25This command will guide you through:
 26  - Adding areas, goals, notebooks, and habits from Lunatask
 27  - Setting default area and notebook
 28  - Configuring and verifying your API key`,
 29	RunE: runInit,
 30}
 31
 32func runInit(cmd *cobra.Command, _ []string) error {
 33	cfg, err := config.Load()
 34	if errors.Is(err, config.ErrNotFound) {
 35		cfg = &config.Config{}
 36
 37		return runFreshSetup(cmd, cfg)
 38	}
 39
 40	if err != nil {
 41		return fmt.Errorf("loading config: %w", err)
 42	}
 43
 44	return runReconfigure(cmd, cfg)
 45}
 46
 47func runFreshSetup(cmd *cobra.Command, cfg *config.Config) error {
 48	printWelcome(cmd)
 49
 50	if err := configureUIPrefs(cfg); err != nil {
 51		return err
 52	}
 53
 54	if err := maybeConfigureAreas(cfg); err != nil {
 55		return err
 56	}
 57
 58	if err := maybeConfigureNotebooks(cfg); err != nil {
 59		return err
 60	}
 61
 62	if err := maybeConfigureHabits(cfg); err != nil {
 63		return err
 64	}
 65
 66	if err := configureDefaults(cfg); err != nil {
 67		return err
 68	}
 69
 70	if err := configureAPIKey(cmd); err != nil {
 71		return err
 72	}
 73
 74	return saveWithSummary(cmd, cfg)
 75}
 76
 77func printWelcome(cmd *cobra.Command) {
 78	out := cmd.OutOrStdout()
 79	fmt.Fprintln(out, ui.Bold.Render("Welcome to lune!"))
 80	fmt.Fprintln(out)
 81	fmt.Fprintln(out, "This wizard will help you configure lune for use with Lunatask.")
 82	fmt.Fprintln(out)
 83	fmt.Fprintln(out, ui.Muted.Render("Since Lunatask is end-to-end encrypted, lune can't fetch your"))
 84	fmt.Fprintln(out, ui.Muted.Render("areas, goals, notebooks, or habits automatically. You'll need to"))
 85	fmt.Fprintln(out, ui.Muted.Render("copy IDs from the Lunatask app."))
 86	fmt.Fprintln(out)
 87	fmt.Fprintln(out, ui.Bold.Render("Where to find IDs:"))
 88	fmt.Fprintln(out, "  Open any item's settings modal → click 'Copy [Item] ID' (bottom left)")
 89	fmt.Fprintln(out)
 90	fmt.Fprintln(out, ui.Muted.Render("You can run 'lune init' again anytime to add or modify config."))
 91	fmt.Fprintln(out)
 92}
 93
 94func runReconfigure(cmd *cobra.Command, cfg *config.Config) error {
 95	fmt.Fprintln(cmd.OutOrStdout(), ui.Bold.Render("lune configuration"))
 96	fmt.Fprintln(cmd.OutOrStdout())
 97
 98	handlers := map[string]func() error{
 99		"areas":     func() error { return manageAreas(cfg) },
100		"notebooks": func() error { return manageNotebooks(cfg) },
101		"habits":    func() error { return manageHabits(cfg) },
102		"defaults":  func() error { return configureDefaults(cfg) },
103		"ui":        func() error { return configureUIPrefs(cfg) },
104		"apikey":    func() error { return configureAPIKey(cmd) },
105		"reset":     func() error { return resetConfig(cmd, cfg) },
106	}
107
108	for {
109		var choice string
110
111		err := huh.NewSelect[string]().
112			Title("What would you like to configure?").
113			Options(
114				huh.NewOption("Manage areas & goals", "areas"),
115				huh.NewOption("Manage notebooks", "notebooks"),
116				huh.NewOption("Manage habits", "habits"),
117				huh.NewOption("Set defaults", "defaults"),
118				huh.NewOption("UI preferences", "ui"),
119				huh.NewOption("API key", "apikey"),
120				huh.NewOption("Reset all configuration", "reset"),
121				huh.NewOption("Done", choiceDone),
122			).
123			Value(&choice).
124			Run()
125		if err != nil {
126			if errors.Is(err, huh.ErrUserAborted) {
127				return nil
128			}
129
130			return err
131		}
132
133		if choice == choiceDone {
134			return saveWithSummary(cmd, cfg)
135		}
136
137		if handler, ok := handlers[choice]; ok {
138			if err := handler(); err != nil {
139				return err
140			}
141		}
142	}
143}
144
145func saveWithSummary(cmd *cobra.Command, cfg *config.Config) error {
146	path, err := config.Path()
147	if err != nil {
148		return err
149	}
150
151	goalCount := 0
152	for _, area := range cfg.Areas {
153		goalCount += len(area.Goals)
154	}
155
156	out := cmd.OutOrStdout()
157	fmt.Fprintln(out)
158	fmt.Fprintln(out, ui.Bold.Render("Configuration summary:"))
159	fmt.Fprintf(out, "  Areas:     %d (%d goals)\n", len(cfg.Areas), goalCount)
160	fmt.Fprintf(out, "  Notebooks: %d\n", len(cfg.Notebooks))
161	fmt.Fprintf(out, "  Habits:    %d\n", len(cfg.Habits))
162
163	if cfg.Defaults.Area != "" {
164		fmt.Fprintf(out, "  Default area: %s\n", cfg.Defaults.Area)
165	}
166
167	if cfg.Defaults.Notebook != "" {
168		fmt.Fprintf(out, "  Default notebook: %s\n", cfg.Defaults.Notebook)
169	}
170
171	fmt.Fprintf(out, "  Color: %s\n", cfg.UI.Color)
172	fmt.Fprintln(out)
173
174	var save bool
175
176	err = huh.NewConfirm().
177		Title("Save configuration?").
178		Affirmative("Save").
179		Negative("Discard").
180		Value(&save).
181		Run()
182	if err != nil {
183		if errors.Is(err, huh.ErrUserAborted) {
184			fmt.Fprintln(out, ui.Warning.Render("Changes discarded."))
185
186			return nil
187		}
188
189		return err
190	}
191
192	if save {
193		if err := cfg.Save(); err != nil {
194			return err
195		}
196
197		fmt.Fprintln(out, ui.Success.Render("Config saved to "+path))
198	} else {
199		fmt.Fprintln(out, ui.Warning.Render("Changes discarded."))
200	}
201
202	return nil
203}