1package tui
2
3import (
4 "fmt"
5 "strings"
6
7 tea "charm.land/bubbletea/v2"
8 "charm.land/lipgloss/v2"
9 "github.com/floatpane/matcha/config"
10)
11
12var (
13 accountItemStyle = lipgloss.NewStyle().PaddingLeft(2)
14 selectedAccountItemStyle = lipgloss.NewStyle().PaddingLeft(2).Foreground(lipgloss.Color("42"))
15 accountEmailStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("240"))
16 dangerStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("196"))
17)
18
19type SettingsState int
20
21const (
22 SettingsMain SettingsState = iota
23 SettingsAccounts
24 SettingsMailingLists
25)
26
27// Settings displays the settings screen.
28type Settings struct {
29 cfg *config.Config
30 state SettingsState
31 cursor int
32 confirmingDelete bool
33 width int
34 height int
35}
36
37// NewSettings creates a new settings model.
38func NewSettings(cfg *config.Config) *Settings {
39 if cfg == nil {
40 cfg = &config.Config{}
41 }
42 return &Settings{
43 cfg: cfg,
44 state: SettingsMain,
45 cursor: 0,
46 }
47}
48
49// Init initializes the settings model.
50func (m *Settings) Init() tea.Cmd {
51 return nil
52}
53
54// Update handles messages for the settings model.
55func (m *Settings) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
56 switch msg := msg.(type) {
57 case tea.WindowSizeMsg:
58 m.width = msg.Width
59 m.height = msg.Height
60 return m, nil
61
62 case tea.KeyPressMsg:
63 if m.state == SettingsMain {
64 return m.updateMain(msg)
65 } else if m.state == SettingsMailingLists {
66 return m.updateMailingLists(msg)
67 } else {
68 return m.updateAccounts(msg)
69 }
70 }
71 return m, nil
72}
73
74func (m *Settings) updateMain(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
75 switch msg.String() {
76 case "up", "k":
77 if m.cursor > 0 {
78 m.cursor--
79 }
80 case "down", "j":
81 // Options: 0: Email Accounts, 1: Image Display, 2: Edit Signature, 3: Contextual Tips, 4: Mailing Lists
82 if m.cursor < 4 {
83 m.cursor++
84 }
85 case "enter":
86 switch m.cursor {
87 case 0: // Email Accounts
88 m.state = SettingsAccounts
89 m.cursor = 0
90 return m, nil
91 case 1: // Image Display
92 m.cfg.DisableImages = !m.cfg.DisableImages
93 // Save config immediately
94 _ = config.SaveConfig(m.cfg)
95 return m, nil
96 case 2: // Edit Signature
97 return m, func() tea.Msg { return GoToSignatureEditorMsg{} }
98 case 3: // Contextual Tips
99 m.cfg.HideTips = !m.cfg.HideTips
100 // Save config immediately
101 _ = config.SaveConfig(m.cfg)
102 return m, nil
103 case 4: // Mailing Lists
104 m.state = SettingsMailingLists
105 m.cursor = 0
106 return m, nil
107 }
108 case "esc":
109 return m, func() tea.Msg { return GoToChoiceMenuMsg{} }
110 }
111 return m, nil
112}
113
114func (m *Settings) updateAccounts(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
115 if m.confirmingDelete {
116 switch msg.String() {
117 case "y", "Y":
118 if m.cursor < len(m.cfg.Accounts) {
119 accountID := m.cfg.Accounts[m.cursor].ID
120 m.confirmingDelete = false
121 return m, func() tea.Msg {
122 return DeleteAccountMsg{AccountID: accountID}
123 }
124 }
125 case "n", "N", "esc":
126 m.confirmingDelete = false
127 return m, nil
128 }
129 return m, nil
130 }
131
132 switch msg.String() {
133 case "up", "k":
134 if m.cursor > 0 {
135 m.cursor--
136 }
137 case "down", "j":
138 // +1 for "Add Account" option
139 if m.cursor < len(m.cfg.Accounts) {
140 m.cursor++
141 }
142 case "d":
143 // Delete selected account (not the "Add Account" option)
144 if m.cursor < len(m.cfg.Accounts) && len(m.cfg.Accounts) > 0 {
145 m.confirmingDelete = true
146 }
147 case "enter":
148 // If cursor is on "Add Account"
149 if m.cursor == len(m.cfg.Accounts) {
150 return m, func() tea.Msg { return GoToAddAccountMsg{} }
151 }
152 case "esc":
153 m.state = SettingsMain
154 m.cursor = 0
155 return m, nil
156 }
157 return m, nil
158}
159
160func (m *Settings) updateMailingLists(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
161 if m.confirmingDelete {
162 switch msg.String() {
163 case "y", "Y":
164 if m.cursor < len(m.cfg.MailingLists) {
165 m.cfg.MailingLists = append(m.cfg.MailingLists[:m.cursor], m.cfg.MailingLists[m.cursor+1:]...)
166 _ = config.SaveConfig(m.cfg)
167 if m.cursor >= len(m.cfg.MailingLists) && m.cursor > 0 {
168 m.cursor--
169 }
170 m.confirmingDelete = false
171 }
172 case "n", "N", "esc":
173 m.confirmingDelete = false
174 return m, nil
175 }
176 return m, nil
177 }
178
179 switch msg.String() {
180 case "up", "k":
181 if m.cursor > 0 {
182 m.cursor--
183 }
184 case "down", "j":
185 if m.cursor < len(m.cfg.MailingLists) {
186 m.cursor++
187 }
188 case "d":
189 if m.cursor < len(m.cfg.MailingLists) && len(m.cfg.MailingLists) > 0 {
190 m.confirmingDelete = true
191 }
192 case "enter":
193 if m.cursor == len(m.cfg.MailingLists) {
194 return m, func() tea.Msg { return GoToAddMailingListMsg{} }
195 }
196 case "esc":
197 m.state = SettingsMain
198 m.cursor = 0
199 return m, nil
200 }
201 return m, nil
202}
203
204// View renders the settings screen.
205func (m *Settings) View() tea.View {
206 if m.state == SettingsMain {
207 return tea.NewView(m.viewMain())
208 } else if m.state == SettingsMailingLists {
209 return tea.NewView(m.viewMailingLists())
210 }
211 return tea.NewView(m.viewAccounts())
212}
213
214func (m *Settings) viewMain() string {
215 var b strings.Builder
216
217 b.WriteString(titleStyle.Render("Settings") + "\n\n")
218
219 // Option 0: Email Accounts
220 if m.cursor == 0 {
221 b.WriteString(selectedAccountItemStyle.Render("> Email Accounts"))
222 } else {
223 b.WriteString(accountItemStyle.Render(" Email Accounts"))
224 }
225 b.WriteString("\n")
226
227 // Option 1: Image Display
228 status := "ON"
229 if m.cfg.DisableImages {
230 status = "OFF"
231 }
232 text := fmt.Sprintf("Image Display: %s", status)
233 if m.cursor == 1 {
234 b.WriteString(selectedAccountItemStyle.Render("> " + text))
235 } else {
236 b.WriteString(accountItemStyle.Render(" " + text))
237 }
238 b.WriteString("\n")
239
240 // Option 2: Edit Signature
241 sigText := "Edit Signature"
242 if config.HasSignature() {
243 sigText = "Edit Signature (configured)"
244 }
245 if m.cursor == 2 {
246 b.WriteString(selectedAccountItemStyle.Render("> " + sigText))
247 } else {
248 b.WriteString(accountItemStyle.Render(" " + sigText))
249 }
250 b.WriteString("\n")
251
252 // Option 3: Contextual Tips
253 tipsStatus := "ON"
254 if m.cfg.HideTips {
255 tipsStatus = "OFF"
256 }
257 tipsText := fmt.Sprintf("Contextual Tips: %s", tipsStatus)
258 if m.cursor == 3 {
259 b.WriteString(selectedAccountItemStyle.Render("> " + tipsText))
260 } else {
261 b.WriteString(accountItemStyle.Render(" " + tipsText))
262 }
263 b.WriteString("\n")
264
265 // Option 4: Mailing Lists
266 mailingListsText := "Mailing Lists"
267 if m.cursor == 4 {
268 b.WriteString(selectedAccountItemStyle.Render("> " + mailingListsText))
269 } else {
270 b.WriteString(accountItemStyle.Render(" " + mailingListsText))
271 }
272 b.WriteString("\n\n")
273
274 if !m.cfg.HideTips {
275 tip := ""
276 switch m.cursor {
277 case 0:
278 tip = "Manage your connected email accounts."
279 case 1:
280 tip = "Toggle displaying images in emails."
281 case 2:
282 tip = "Configure the signature appended to your outgoing emails."
283 case 3:
284 tip = "Toggle displaying helpful contextual tips like this one."
285 case 4:
286 tip = "Manage groups of email addresses to quickly send to multiple people."
287 }
288 if tip != "" {
289 b.WriteString(TipStyle.Render("Tip: "+tip) + "\n\n")
290 }
291 }
292
293 mainContent := b.String()
294 helpView := helpStyle.Render("↑/↓: navigate • enter: select/toggle • esc: back")
295
296 if m.height > 0 {
297 currentHeight := lipgloss.Height(docStyle.Render(mainContent + helpView))
298 gap := m.height - currentHeight
299 if gap > 0 {
300 mainContent += strings.Repeat("\n", gap)
301 }
302 } else {
303 mainContent += "\n\n"
304 }
305
306 return docStyle.Render(mainContent + helpView)
307}
308
309func (m *Settings) viewAccounts() string {
310 var b strings.Builder
311
312 b.WriteString(titleStyle.Render("Account Settings"))
313 b.WriteString("\n\n")
314
315 if len(m.cfg.Accounts) == 0 {
316 b.WriteString(accountEmailStyle.Render(" No accounts configured.\n"))
317 b.WriteString("\n")
318 }
319
320 for i, account := range m.cfg.Accounts {
321 displayName := account.Email
322 if account.Name != "" {
323 displayName = fmt.Sprintf("%s (%s)", account.Name, account.FetchEmail)
324 }
325
326 providerInfo := account.ServiceProvider
327 if account.ServiceProvider == "custom" {
328 providerInfo = fmt.Sprintf("custom: %s", account.IMAPServer)
329 }
330
331 line := fmt.Sprintf("%s - %s", displayName, accountEmailStyle.Render(providerInfo))
332
333 if m.cursor == i {
334 b.WriteString(selectedAccountItemStyle.Render(fmt.Sprintf("> %s", line)))
335 } else {
336 b.WriteString(accountItemStyle.Render(fmt.Sprintf(" %s", line)))
337 }
338 b.WriteString("\n")
339 }
340
341 // Add Account option
342 addAccountText := "Add New Account"
343 if m.cursor == len(m.cfg.Accounts) {
344 b.WriteString(selectedAccountItemStyle.Render(fmt.Sprintf("> %s", addAccountText)))
345 } else {
346 b.WriteString(accountItemStyle.Render(fmt.Sprintf(" %s", addAccountText)))
347 }
348 b.WriteString("\n")
349
350 mainContent := b.String()
351 helpView := helpStyle.Render("↑/↓: navigate • enter: select • d: delete account • esc: back")
352
353 if m.height > 0 {
354 currentHeight := lipgloss.Height(docStyle.Render(mainContent + helpView))
355 gap := m.height - currentHeight
356 if gap > 0 {
357 mainContent += strings.Repeat("\n", gap)
358 }
359 } else {
360 mainContent += "\n\n"
361 }
362
363 if m.confirmingDelete {
364 accountName := m.cfg.Accounts[m.cursor].Email
365 dialog := DialogBoxStyle.Render(
366 lipgloss.JoinVertical(lipgloss.Center,
367 dangerStyle.Render("Delete account?"),
368 accountEmailStyle.Render(accountName),
369 HelpStyle.Render("\n(y/n)"),
370 ),
371 )
372 return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, dialog)
373 }
374
375 return docStyle.Render(mainContent + helpView)
376}
377
378func (m *Settings) viewMailingLists() string {
379 var b strings.Builder
380
381 b.WriteString(titleStyle.Render("Mailing Lists"))
382 b.WriteString("\n\n")
383
384 if len(m.cfg.MailingLists) == 0 {
385 b.WriteString(accountEmailStyle.Render(" No mailing lists configured.\n"))
386 b.WriteString("\n")
387 }
388
389 for i, list := range m.cfg.MailingLists {
390 displayName := list.Name
391
392 addrCount := fmt.Sprintf("%d address", len(list.Addresses))
393 if len(list.Addresses) != 1 {
394 addrCount += "es"
395 }
396
397 line := fmt.Sprintf("%s - %s", displayName, accountEmailStyle.Render(addrCount))
398
399 if m.cursor == i {
400 b.WriteString(selectedAccountItemStyle.Render(fmt.Sprintf("> %s", line)))
401 } else {
402 b.WriteString(accountItemStyle.Render(fmt.Sprintf(" %s", line)))
403 }
404 b.WriteString("\n")
405 }
406
407 // Add Mailing List option
408 addListText := "Add New Mailing List"
409 if m.cursor == len(m.cfg.MailingLists) {
410 b.WriteString(selectedAccountItemStyle.Render(fmt.Sprintf("> %s", addListText)))
411 } else {
412 b.WriteString(accountItemStyle.Render(fmt.Sprintf(" %s", addListText)))
413 }
414 b.WriteString("\n")
415
416 helpView := helpStyle.Render("↑/↓: navigate • enter: select • d: delete • esc: back")
417 mainContent := b.String()
418
419 if m.height > 0 {
420 contentHeight := strings.Count(mainContent, "\n") + 1
421 gap := m.height - contentHeight - 2 // -2 for margins
422 if gap > 0 {
423 mainContent += strings.Repeat("\n", gap)
424 }
425 } else {
426 mainContent += "\n\n"
427 }
428
429 if m.confirmingDelete {
430 listName := m.cfg.MailingLists[m.cursor].Name
431 dialog := DialogBoxStyle.Render(
432 lipgloss.JoinVertical(lipgloss.Center,
433 dangerStyle.Render("Delete mailing list?"),
434 accountEmailStyle.Render(listName),
435 HelpStyle.Render("\n(y/n)"),
436 ),
437 )
438 return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, dialog)
439 }
440
441 return docStyle.Render(mainContent + helpView)
442}
443
444// UpdateConfig updates the configuration (used when accounts are deleted).
445func (m *Settings) UpdateConfig(cfg *config.Config) {
446 m.cfg = cfg
447 if m.state == SettingsAccounts && m.cursor >= len(cfg.Accounts) {
448 m.cursor = len(cfg.Accounts)
449 }
450}