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}