1// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
2//
3// SPDX-License-Identifier: AGPL-3.0-or-later
4
5package init
6
7import (
8 "errors"
9 "fmt"
10
11 "github.com/charmbracelet/huh"
12
13 "git.secluded.site/lune/internal/config"
14)
15
16func manageNotebooks(cfg *config.Config) error {
17 nav := manageNotebooksAsStep(cfg)
18 if nav == navQuit {
19 return errQuit
20 }
21
22 return nil
23}
24
25// manageNotebooksAsStep runs notebooks management as a wizard step with Back/Next navigation.
26func manageNotebooksAsStep(cfg *config.Config) wizardNav {
27 for {
28 options := buildNotebookStepOptions(cfg.Notebooks)
29
30 choice, err := runListSelect(
31 "Notebooks",
32 "Notebooks organize your notes in Lunatask.",
33 options,
34 )
35 if err != nil {
36 return navQuit
37 }
38
39 switch choice {
40 case choiceBack:
41 return navBack
42 case choiceNext:
43 return navNext
44 case choiceAdd:
45 if err := addNotebook(cfg); errors.Is(err, errQuit) {
46 return navQuit
47 }
48 default:
49 idx, ok := parseEditIndex(choice)
50 if !ok {
51 continue
52 }
53
54 if err := manageNotebookActions(cfg, idx); errors.Is(err, errQuit) {
55 return navQuit
56 }
57 }
58 }
59}
60
61func buildNotebookStepOptions(notebooks []config.Notebook) []huh.Option[string] {
62 options := []huh.Option[string]{
63 huh.NewOption("Add new notebook", choiceAdd),
64 }
65
66 for idx, notebook := range notebooks {
67 label := fmt.Sprintf("%s (%s)", notebook.Name, notebook.Key)
68 options = append(options, huh.NewOption(label, fmt.Sprintf("edit:%d", idx)))
69 }
70
71 options = append(options,
72 huh.NewOption("← Back", choiceBack),
73 huh.NewOption("Next →", choiceNext),
74 )
75
76 return options
77}
78
79func addNotebook(cfg *config.Config) error {
80 notebook, err := editNotebook(nil, cfg)
81 if err != nil {
82 if errors.Is(err, errBack) {
83 return nil
84 }
85
86 return err
87 }
88
89 cfg.Notebooks = append(cfg.Notebooks, *notebook)
90
91 return nil
92}
93
94func manageNotebookActions(cfg *config.Config, idx int) error {
95 if idx < 0 || idx >= len(cfg.Notebooks) {
96 return fmt.Errorf("%w: notebook %d", errIndexOutRange, idx)
97 }
98
99 notebook := &cfg.Notebooks[idx]
100
101 action, err := runActionSelect(fmt.Sprintf("Notebook: %s (%s)", notebook.Name, notebook.Key), false)
102 if err != nil {
103 return err
104 }
105
106 switch action {
107 case itemActionEdit:
108 updated, err := editNotebook(notebook, cfg)
109 if err != nil {
110 if errors.Is(err, errBack) {
111 return nil
112 }
113
114 return err
115 }
116
117 if updated != nil {
118 cfg.Notebooks[idx] = *updated
119 }
120 case itemActionDelete:
121 return deleteNotebook(cfg, idx)
122 case itemActionNone, itemActionGoals:
123 // User cancelled or went back; goals not applicable here
124 }
125
126 return nil
127}
128
129func editNotebook(existing *config.Notebook, cfg *config.Config) (*config.Notebook, error) {
130 notebook := config.Notebook{}
131 if existing != nil {
132 notebook = *existing
133 }
134
135 err := runItemForm(¬ebook.Name, ¬ebook.Key, ¬ebook.ID, itemFormConfig{
136 itemType: "notebook",
137 namePlaceholder: "Gaelic Notes",
138 keyPlaceholder: "gaelic",
139 keyValidator: validateNotebookKey(cfg, existing),
140 supportsDeepLink: true,
141 })
142 if err != nil {
143 return nil, err
144 }
145
146 return ¬ebook, nil
147}
148
149func validateNotebookKey(cfg *config.Config, existing *config.Notebook) func(string) error {
150 return func(input string) error {
151 if err := validateKeyFormat(input); err != nil {
152 return err
153 }
154
155 for idx := range cfg.Notebooks {
156 if existing != nil && &cfg.Notebooks[idx] == existing {
157 continue
158 }
159
160 if cfg.Notebooks[idx].Key == input {
161 return errKeyDuplicate
162 }
163 }
164
165 return nil
166 }
167}
168
169func deleteNotebook(cfg *config.Config, idx int) error {
170 if idx < 0 || idx >= len(cfg.Notebooks) {
171 return fmt.Errorf("%w: notebook %d", errIndexOutRange, idx)
172 }
173
174 notebook := cfg.Notebooks[idx]
175
176 var confirm bool
177
178 err := huh.NewConfirm().
179 Title(fmt.Sprintf("Delete notebook '%s'?", notebook.Name)).
180 Description("This cannot be undone.").
181 Affirmative("Delete").
182 Negative("Cancel").
183 Value(&confirm).
184 Run()
185 if err != nil {
186 if errors.Is(err, huh.ErrUserAborted) {
187 return errQuit
188 }
189
190 return err
191 }
192
193 if confirm {
194 cfg.Notebooks = append(cfg.Notebooks[:idx], cfg.Notebooks[idx+1:]...)
195 if cfg.Defaults.Notebook == notebook.Key {
196 cfg.Defaults.Notebook = ""
197 }
198 }
199
200 return nil
201}