1package tui
2
3import (
4 "fmt"
5 "strings"
6
7 "charm.land/bubbles/v2/textinput"
8 tea "charm.land/bubbletea/v2"
9 "charm.land/lipgloss/v2"
10 "github.com/floatpane/matcha/config"
11 "github.com/floatpane/matcha/theme"
12)
13
14var (
15 accountItemStyle = lipgloss.NewStyle().PaddingLeft(2)
16 selectedAccountItemStyle = lipgloss.NewStyle().PaddingLeft(2).Foreground(lipgloss.Color("42"))
17 accountEmailStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("240"))
18 dangerStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("196"))
19
20 settingsFocusedStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("42"))
21 settingsBlurredStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("240"))
22)
23
24type SettingsState int
25
26const (
27 SettingsMain SettingsState = iota
28 SettingsAccounts
29 SettingsMailingLists
30 SettingsSMIMEConfig
31 SettingsTheme
32 SettingsEncryption
33)
34
35// Settings displays the settings screen.
36type Settings struct {
37 cfg *config.Config
38 state SettingsState
39 cursor int
40 confirmingDelete bool
41 width int
42 height int
43
44 // S/MIME Config fields
45 editingAccountIdx int
46 focusIndex int
47 smimeCertInput textinput.Model
48 smimeKeyInput textinput.Model
49
50 // PGP Config fields
51 pgpPublicKeyInput textinput.Model
52 pgpPrivateKeyInput textinput.Model
53 pgpKeySource string // "file" or "yubikey"
54 pgpPINInput textinput.Model
55
56 // Encryption fields
57 encPasswordInput textinput.Model
58 encConfirmInput textinput.Model
59 encFocusIndex int
60 encError string
61 encEnabling bool // true when enabling encryption in progress
62 confirmingDisable bool // true when confirming disable
63}
64
65// NewSettings creates a new settings model.
66func NewSettings(cfg *config.Config) *Settings {
67 if cfg == nil {
68 cfg = &config.Config{}
69 }
70
71 tiStyles := ThemedTextInputStyles()
72
73 certInput := textinput.New()
74 certInput.Placeholder = "/path/to/cert.pem"
75 certInput.Prompt = "> "
76 certInput.CharLimit = 256
77 certInput.SetStyles(tiStyles)
78
79 keyInput := textinput.New()
80 keyInput.Placeholder = "/path/to/private_key.pem"
81 keyInput.Prompt = "> "
82 keyInput.CharLimit = 256
83 keyInput.SetStyles(tiStyles)
84
85 pgpPubInput := textinput.New()
86 pgpPubInput.Placeholder = "/path/to/public_key.asc"
87 pgpPubInput.Prompt = "> "
88 pgpPubInput.CharLimit = 256
89 pgpPubInput.SetStyles(tiStyles)
90
91 pgpPrivInput := textinput.New()
92 pgpPrivInput.Placeholder = "/path/to/private_key.asc"
93 pgpPrivInput.Prompt = "> "
94 pgpPrivInput.CharLimit = 256
95 pgpPrivInput.SetStyles(tiStyles)
96
97 pgpPINInput := textinput.New()
98 pgpPINInput.Placeholder = "YubiKey PIN (6-8 digits)"
99 pgpPINInput.Prompt = "> "
100 pgpPINInput.CharLimit = 16
101 pgpPINInput.EchoMode = textinput.EchoPassword
102 pgpPINInput.EchoCharacter = '*'
103 pgpPINInput.SetStyles(tiStyles)
104
105 encPassInput := textinput.New()
106 encPassInput.Placeholder = "Password"
107 encPassInput.Prompt = "> "
108 encPassInput.CharLimit = 256
109 encPassInput.EchoMode = textinput.EchoPassword
110 encPassInput.EchoCharacter = '*'
111 encPassInput.SetStyles(tiStyles)
112
113 encConfInput := textinput.New()
114 encConfInput.Placeholder = "Confirm Password"
115 encConfInput.Prompt = "> "
116 encConfInput.CharLimit = 256
117 encConfInput.EchoMode = textinput.EchoPassword
118 encConfInput.EchoCharacter = '*'
119 encConfInput.SetStyles(tiStyles)
120
121 return &Settings{
122 cfg: cfg,
123 state: SettingsMain,
124 cursor: 0,
125 smimeCertInput: certInput,
126 smimeKeyInput: keyInput,
127 pgpPublicKeyInput: pgpPubInput,
128 pgpPrivateKeyInput: pgpPrivInput,
129 pgpKeySource: "file", // Default to file-based keys
130 pgpPINInput: pgpPINInput,
131 encPasswordInput: encPassInput,
132 encConfirmInput: encConfInput,
133 }
134}
135
136// Init initializes the settings model.
137func (m *Settings) Init() tea.Cmd {
138 return textinput.Blink
139}
140
141// Update handles messages for the settings model.
142func (m *Settings) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
143 var cmds []tea.Cmd
144 var cmd tea.Cmd
145
146 switch msg := msg.(type) {
147 case tea.WindowSizeMsg:
148 m.width = msg.Width
149 m.height = msg.Height
150 m.smimeCertInput.SetWidth(m.width - 6)
151 m.smimeKeyInput.SetWidth(m.width - 6)
152 m.pgpPublicKeyInput.SetWidth(m.width - 6)
153 m.pgpPrivateKeyInput.SetWidth(m.width - 6)
154 m.pgpPINInput.SetWidth(m.width - 6)
155 return m, nil
156
157 case tea.KeyPressMsg:
158 if m.state == SettingsMain {
159 return m.updateMain(msg)
160 } else if m.state == SettingsTheme {
161 return m.updateTheme(msg)
162 } else if m.state == SettingsMailingLists {
163 return m.updateMailingLists(msg)
164 } else if m.state == SettingsSMIMEConfig {
165 var m2 *Settings
166 m2, cmd = m.updateSMIMEConfig(msg)
167 cmds = append(cmds, cmd)
168 return m2, tea.Batch(cmds...)
169 } else if m.state == SettingsEncryption {
170 return m.updateEncryption(msg)
171 } else {
172 return m.updateAccounts(msg)
173 }
174
175 case SecureModeEnabledMsg:
176 m.encEnabling = false
177 if msg.Err != nil {
178 m.encError = msg.Err.Error()
179 return m, nil
180 }
181 m.state = SettingsMain
182 m.cursor = 7
183 return m, nil
184
185 case SecureModeDisabledMsg:
186 if msg.Err != nil {
187 m.encError = msg.Err.Error()
188 return m, nil
189 }
190 m.confirmingDisable = false
191 m.state = SettingsMain
192 m.cursor = 7
193 return m, nil
194 }
195
196 if m.state == SettingsEncryption {
197 m.encPasswordInput, cmd = m.encPasswordInput.Update(msg)
198 cmds = append(cmds, cmd)
199 m.encConfirmInput, cmd = m.encConfirmInput.Update(msg)
200 cmds = append(cmds, cmd)
201 }
202
203 if m.state == SettingsSMIMEConfig {
204 m.smimeCertInput, cmd = m.smimeCertInput.Update(msg)
205 cmds = append(cmds, cmd)
206 m.smimeKeyInput, cmd = m.smimeKeyInput.Update(msg)
207 cmds = append(cmds, cmd)
208 m.pgpPublicKeyInput, cmd = m.pgpPublicKeyInput.Update(msg)
209 cmds = append(cmds, cmd)
210 m.pgpPrivateKeyInput, cmd = m.pgpPrivateKeyInput.Update(msg)
211 cmds = append(cmds, cmd)
212 m.pgpPINInput, cmd = m.pgpPINInput.Update(msg)
213 cmds = append(cmds, cmd)
214 }
215
216 return m, tea.Batch(cmds...)
217}
218
219func (m *Settings) updateMain(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
220 switch msg.String() {
221 case "up", "k":
222 if m.cursor > 0 {
223 m.cursor--
224 }
225 case "down", "j":
226 // Options: 0: Email Accounts, 1: Theme, 2: Image Display, 3: Edit Signature, 4: Contextual Tips, 5: Desktop Notifications, 6: Mailing Lists, 7: Encryption
227 if m.cursor < 7 {
228 m.cursor++
229 }
230 case "enter":
231 switch m.cursor {
232 case 0: // Email Accounts
233 m.state = SettingsAccounts
234 m.cursor = 0
235 return m, nil
236 case 1: // Theme
237 m.state = SettingsTheme
238 // Position cursor on the currently active theme
239 themes := theme.AllThemes()
240 m.cursor = 0
241 for i, t := range themes {
242 if t.Name == theme.ActiveTheme.Name {
243 m.cursor = i
244 break
245 }
246 }
247 return m, nil
248 case 2: // Image Display
249 m.cfg.DisableImages = !m.cfg.DisableImages
250 _ = config.SaveConfig(m.cfg)
251 return m, nil
252 case 3: // Edit Signature
253 return m, func() tea.Msg { return GoToSignatureEditorMsg{} }
254 case 4: // Contextual Tips
255 m.cfg.HideTips = !m.cfg.HideTips
256 _ = config.SaveConfig(m.cfg)
257 return m, nil
258 case 5: // Desktop Notifications
259 m.cfg.DisableNotifications = !m.cfg.DisableNotifications
260 _ = config.SaveConfig(m.cfg)
261 return m, nil
262 case 6: // Mailing Lists
263 m.state = SettingsMailingLists
264 m.cursor = 0
265 return m, nil
266 case 7: // Encryption
267 m.state = SettingsEncryption
268 m.encError = ""
269 m.encPasswordInput.SetValue("")
270 m.encConfirmInput.SetValue("")
271 m.encFocusIndex = 0
272 m.confirmingDisable = false
273 m.encEnabling = false
274 if !config.IsSecureModeEnabled() {
275 m.encPasswordInput.Focus()
276 m.encConfirmInput.Blur()
277 return m, textinput.Blink
278 }
279 return m, nil
280 }
281 case "esc":
282 return m, func() tea.Msg { return GoToChoiceMenuMsg{} }
283 }
284 return m, nil
285}
286
287func (m *Settings) updateAccounts(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
288 if m.confirmingDelete {
289 switch msg.String() {
290 case "y", "Y":
291 if m.cursor < len(m.cfg.Accounts) {
292 accountID := m.cfg.Accounts[m.cursor].ID
293 m.confirmingDelete = false
294 return m, func() tea.Msg {
295 return DeleteAccountMsg{AccountID: accountID}
296 }
297 }
298 case "n", "N", "esc":
299 m.confirmingDelete = false
300 return m, nil
301 }
302 return m, nil
303 }
304
305 switch msg.String() {
306 case "up", "k":
307 if m.cursor > 0 {
308 m.cursor--
309 }
310 case "down", "j":
311 // +1 for "Add Account" option
312 if m.cursor < len(m.cfg.Accounts) {
313 m.cursor++
314 }
315 case "d":
316 // Delete selected account (not the "Add Account" option)
317 if m.cursor < len(m.cfg.Accounts) && len(m.cfg.Accounts) > 0 {
318 m.confirmingDelete = true
319 }
320 case "e":
321 // Edit selected account
322 if m.cursor < len(m.cfg.Accounts) {
323 acc := m.cfg.Accounts[m.cursor]
324 return m, func() tea.Msg {
325 return GoToEditAccountMsg{
326 AccountID: acc.ID,
327 Provider: acc.ServiceProvider,
328 Name: acc.Name,
329 Email: acc.Email,
330 FetchEmail: acc.FetchEmail,
331 SendAsEmail: acc.SendAsEmail,
332 IMAPServer: acc.IMAPServer,
333 IMAPPort: acc.IMAPPort,
334 SMTPServer: acc.SMTPServer,
335 SMTPPort: acc.SMTPPort,
336 Protocol: acc.Protocol,
337 JMAPEndpoint: acc.JMAPEndpoint,
338 POP3Server: acc.POP3Server,
339 POP3Port: acc.POP3Port,
340 }
341 }
342 }
343 case "enter":
344 // If cursor is on "Add Account"
345 if m.cursor == len(m.cfg.Accounts) {
346 return m, func() tea.Msg { return GoToAddAccountMsg{} }
347 } else if m.cursor < len(m.cfg.Accounts) {
348 m.editingAccountIdx = m.cursor
349 m.state = SettingsSMIMEConfig
350 m.smimeCertInput.SetValue(m.cfg.Accounts[m.cursor].SMIMECert)
351 m.smimeKeyInput.SetValue(m.cfg.Accounts[m.cursor].SMIMEKey)
352 m.pgpPublicKeyInput.SetValue(m.cfg.Accounts[m.cursor].PGPPublicKey)
353 m.pgpPrivateKeyInput.SetValue(m.cfg.Accounts[m.cursor].PGPPrivateKey)
354 // Initialize PGP key source
355 if m.cfg.Accounts[m.cursor].PGPKeySource == "" {
356 m.pgpKeySource = "file"
357 } else {
358 m.pgpKeySource = m.cfg.Accounts[m.cursor].PGPKeySource
359 }
360 m.pgpPINInput.SetValue(m.cfg.Accounts[m.cursor].PGPPIN)
361 m.focusIndex = 0
362 m.smimeCertInput.Focus()
363 m.smimeKeyInput.Blur()
364 m.pgpPublicKeyInput.Blur()
365 m.pgpPrivateKeyInput.Blur()
366 m.pgpPINInput.Blur()
367 return m, textinput.Blink
368 }
369 case "esc":
370 m.state = SettingsMain
371 m.cursor = 0
372 return m, nil
373 }
374 return m, nil
375}
376
377// Focus indices for the crypto config screen:
378// 0: S/MIME Certificate Path
379// 1: S/MIME Private Key Path
380// 2: S/MIME Sign By Default toggle
381// 3: PGP Public Key Path
382// 4: PGP Private Key Path
383// 5: PGP Key Source toggle (file/yubikey)
384// 6: PGP PIN input (only shown if yubikey)
385// 7: PGP Sign By Default toggle
386// 8: Save button
387// 9: Cancel button
388const cryptoConfigMaxFocus = 9
389
390func (m *Settings) updateSMIMEConfig(msg tea.KeyPressMsg) (*Settings, tea.Cmd) {
391 var cmds []tea.Cmd
392 var cmd tea.Cmd
393
394 switch msg.String() {
395 case "esc":
396 m.state = SettingsAccounts
397 return m, nil
398 case "tab", "shift+tab", "up", "down":
399 if msg.String() == "shift+tab" || msg.String() == "up" {
400 m.focusIndex--
401 if m.focusIndex < 0 {
402 m.focusIndex = cryptoConfigMaxFocus
403 }
404 } else {
405 m.focusIndex++
406 if m.focusIndex > cryptoConfigMaxFocus {
407 m.focusIndex = 0
408 }
409 }
410
411 m.smimeCertInput.Blur()
412 m.smimeKeyInput.Blur()
413 m.pgpPublicKeyInput.Blur()
414 m.pgpPrivateKeyInput.Blur()
415 m.pgpPINInput.Blur()
416
417 switch m.focusIndex {
418 case 0:
419 cmds = append(cmds, m.smimeCertInput.Focus())
420 case 1:
421 cmds = append(cmds, m.smimeKeyInput.Focus())
422 case 3:
423 cmds = append(cmds, m.pgpPublicKeyInput.Focus())
424 case 4:
425 cmds = append(cmds, m.pgpPrivateKeyInput.Focus())
426 case 6:
427 cmds = append(cmds, m.pgpPINInput.Focus())
428 }
429 return m, tea.Batch(cmds...)
430 case "enter", " ":
431 switch m.focusIndex {
432 case 0: // S/MIME cert - enter advances to next field
433 if msg.String() == "enter" {
434 m.focusIndex = 1
435 m.smimeCertInput.Blur()
436 cmds = append(cmds, m.smimeKeyInput.Focus())
437 return m, tea.Batch(cmds...)
438 }
439 case 1: // S/MIME key - enter advances
440 if msg.String() == "enter" {
441 m.focusIndex = 2
442 m.smimeKeyInput.Blur()
443 return m, nil
444 }
445 case 2: // S/MIME sign toggle
446 if msg.String() == "enter" || msg.String() == " " {
447 m.cfg.Accounts[m.editingAccountIdx].SMIMESignByDefault = !m.cfg.Accounts[m.editingAccountIdx].SMIMESignByDefault
448 }
449 return m, nil
450 case 3: // PGP public key - enter advances
451 if msg.String() == "enter" {
452 m.focusIndex = 4
453 m.pgpPublicKeyInput.Blur()
454 cmds = append(cmds, m.pgpPrivateKeyInput.Focus())
455 return m, tea.Batch(cmds...)
456 }
457 case 4: // PGP private key - enter advances
458 if msg.String() == "enter" {
459 m.focusIndex = 5
460 m.pgpPrivateKeyInput.Blur()
461 return m, nil
462 }
463 case 5: // PGP key source toggle (file/yubikey)
464 if msg.String() == "enter" || msg.String() == " " {
465 if m.pgpKeySource == "file" {
466 m.pgpKeySource = "yubikey"
467 } else {
468 m.pgpKeySource = "file"
469 }
470 }
471 return m, nil
472 case 6: // PGP PIN input - enter advances
473 if msg.String() == "enter" {
474 m.focusIndex = 7
475 m.pgpPINInput.Blur()
476 return m, nil
477 }
478 case 7: // PGP sign toggle
479 if msg.String() == "enter" || msg.String() == " " {
480 m.cfg.Accounts[m.editingAccountIdx].PGPSignByDefault = !m.cfg.Accounts[m.editingAccountIdx].PGPSignByDefault
481 }
482 return m, nil
483 case 8: // Save
484 if msg.String() == "enter" {
485 m.cfg.Accounts[m.editingAccountIdx].SMIMECert = m.smimeCertInput.Value()
486 m.cfg.Accounts[m.editingAccountIdx].SMIMEKey = m.smimeKeyInput.Value()
487 m.cfg.Accounts[m.editingAccountIdx].PGPPublicKey = m.pgpPublicKeyInput.Value()
488 m.cfg.Accounts[m.editingAccountIdx].PGPPrivateKey = m.pgpPrivateKeyInput.Value()
489 m.cfg.Accounts[m.editingAccountIdx].PGPKeySource = m.pgpKeySource
490 m.cfg.Accounts[m.editingAccountIdx].PGPPIN = m.pgpPINInput.Value()
491 _ = config.SaveConfig(m.cfg)
492 m.state = SettingsAccounts
493 return m, nil
494 }
495 case 9: // Cancel
496 if msg.String() == "enter" {
497 m.state = SettingsAccounts
498 return m, nil
499 }
500 }
501 }
502
503 switch m.focusIndex {
504 case 0:
505 m.smimeCertInput, cmd = m.smimeCertInput.Update(msg)
506 cmds = append(cmds, cmd)
507 case 1:
508 m.smimeKeyInput, cmd = m.smimeKeyInput.Update(msg)
509 cmds = append(cmds, cmd)
510 case 3:
511 m.pgpPublicKeyInput, cmd = m.pgpPublicKeyInput.Update(msg)
512 cmds = append(cmds, cmd)
513 case 4:
514 m.pgpPrivateKeyInput, cmd = m.pgpPrivateKeyInput.Update(msg)
515 cmds = append(cmds, cmd)
516 case 6:
517 m.pgpPINInput, cmd = m.pgpPINInput.Update(msg)
518 cmds = append(cmds, cmd)
519 }
520
521 return m, tea.Batch(cmds...)
522}
523
524func (m *Settings) updateMailingLists(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
525 if m.confirmingDelete {
526 switch msg.String() {
527 case "y", "Y":
528 if m.cursor < len(m.cfg.MailingLists) {
529 m.cfg.MailingLists = append(m.cfg.MailingLists[:m.cursor], m.cfg.MailingLists[m.cursor+1:]...)
530 _ = config.SaveConfig(m.cfg)
531 if m.cursor >= len(m.cfg.MailingLists) && m.cursor > 0 {
532 m.cursor--
533 }
534 m.confirmingDelete = false
535 }
536 case "n", "N", "esc":
537 m.confirmingDelete = false
538 return m, nil
539 }
540 return m, nil
541 }
542
543 switch msg.String() {
544 case "up", "k":
545 if m.cursor > 0 {
546 m.cursor--
547 }
548 case "down", "j":
549 if m.cursor < len(m.cfg.MailingLists) {
550 m.cursor++
551 }
552 case "d":
553 if m.cursor < len(m.cfg.MailingLists) && len(m.cfg.MailingLists) > 0 {
554 m.confirmingDelete = true
555 }
556 case "e":
557 // Edit selected mailing list
558 if m.cursor < len(m.cfg.MailingLists) {
559 list := m.cfg.MailingLists[m.cursor]
560 idx := m.cursor
561 return m, func() tea.Msg {
562 return GoToEditMailingListMsg{
563 Index: idx,
564 Name: list.Name,
565 Addresses: strings.Join(list.Addresses, ", "),
566 }
567 }
568 }
569 case "enter":
570 if m.cursor == len(m.cfg.MailingLists) {
571 return m, func() tea.Msg { return GoToAddMailingListMsg{} }
572 }
573 case "esc":
574 m.state = SettingsMain
575 m.cursor = 0
576 return m, nil
577 }
578 return m, nil
579}
580
581// View renders the settings screen.
582func (m *Settings) View() tea.View {
583 if m.state == SettingsMain {
584 return tea.NewView(m.viewMain())
585 } else if m.state == SettingsTheme {
586 return tea.NewView(m.viewTheme())
587 } else if m.state == SettingsMailingLists {
588 return tea.NewView(m.viewMailingLists())
589 } else if m.state == SettingsSMIMEConfig {
590 return tea.NewView(m.viewSMIMEConfig())
591 } else if m.state == SettingsEncryption {
592 return tea.NewView(m.viewEncryption())
593 }
594 return tea.NewView(m.viewAccounts())
595}
596
597func (m *Settings) viewMain() string {
598 var b strings.Builder
599
600 b.WriteString(titleStyle.Render("Settings") + "\n\n")
601
602 // Option 0: Email Accounts
603 if m.cursor == 0 {
604 b.WriteString(selectedAccountItemStyle.Render("> Email Accounts"))
605 } else {
606 b.WriteString(accountItemStyle.Render(" Email Accounts"))
607 }
608 b.WriteString("\n")
609
610 // Option 1: Theme
611 themeText := fmt.Sprintf("Theme: %s", theme.ActiveTheme.Name)
612 if m.cursor == 1 {
613 b.WriteString(selectedAccountItemStyle.Render("> " + themeText))
614 } else {
615 b.WriteString(accountItemStyle.Render(" " + themeText))
616 }
617 b.WriteString("\n")
618
619 // Option 2: Image Display
620 status := "ON"
621 if m.cfg.DisableImages {
622 status = "OFF"
623 }
624 text := fmt.Sprintf("Image Display: %s", status)
625 if m.cursor == 2 {
626 b.WriteString(selectedAccountItemStyle.Render("> " + text))
627 } else {
628 b.WriteString(accountItemStyle.Render(" " + text))
629 }
630 b.WriteString("\n")
631
632 // Option 3: Edit Signature
633 sigText := "Edit Signature"
634 if config.HasSignature() {
635 sigText = "Edit Signature (configured)"
636 }
637 if m.cursor == 3 {
638 b.WriteString(selectedAccountItemStyle.Render("> " + sigText))
639 } else {
640 b.WriteString(accountItemStyle.Render(" " + sigText))
641 }
642 b.WriteString("\n")
643
644 // Option 4: Contextual Tips
645 tipsStatus := "ON"
646 if m.cfg.HideTips {
647 tipsStatus = "OFF"
648 }
649 tipsText := fmt.Sprintf("Contextual Tips: %s", tipsStatus)
650 if m.cursor == 4 {
651 b.WriteString(selectedAccountItemStyle.Render("> " + tipsText))
652 } else {
653 b.WriteString(accountItemStyle.Render(" " + tipsText))
654 }
655 b.WriteString("\n")
656
657 // Option 5: Desktop Notifications
658 notifStatus := "ON"
659 if m.cfg.DisableNotifications {
660 notifStatus = "OFF"
661 }
662 notifText := fmt.Sprintf("Desktop Notifications: %s", notifStatus)
663 if m.cursor == 5 {
664 b.WriteString(selectedAccountItemStyle.Render("> " + notifText))
665 } else {
666 b.WriteString(accountItemStyle.Render(" " + notifText))
667 }
668 b.WriteString("\n")
669
670 // Option 6: Mailing Lists
671 mailingListsText := "Mailing Lists"
672 if m.cursor == 6 {
673 b.WriteString(selectedAccountItemStyle.Render("> " + mailingListsText))
674 } else {
675 b.WriteString(accountItemStyle.Render(" " + mailingListsText))
676 }
677 b.WriteString("\n")
678
679 // Option 7: Encryption
680 encStatus := "OFF"
681 if config.IsSecureModeEnabled() {
682 encStatus = "ON"
683 }
684 encText := fmt.Sprintf("Encryption: %s", encStatus)
685 if m.cursor == 7 {
686 b.WriteString(selectedAccountItemStyle.Render("> " + encText))
687 } else {
688 b.WriteString(accountItemStyle.Render(" " + encText))
689 }
690 b.WriteString("\n\n")
691
692 if !m.cfg.HideTips {
693 tip := ""
694 switch m.cursor {
695 case 0:
696 tip = "Manage your connected email accounts."
697 case 1:
698 tip = "Choose a color theme for the application."
699 case 2:
700 tip = "Toggle displaying images in emails."
701 case 3:
702 tip = "Configure the signature appended to your outgoing emails."
703 case 4:
704 tip = "Toggle displaying helpful contextual tips like this one."
705 case 5:
706 tip = "Toggle desktop notifications when new mail arrives."
707 case 6:
708 tip = "Manage groups of email addresses to quickly send to multiple people."
709 case 7:
710 tip = "Encrypt all data with a password. You'll need to enter it each time you open matcha."
711 }
712 if tip != "" {
713 b.WriteString(TipStyle.Render("Tip: "+tip) + "\n\n")
714 }
715 }
716
717 mainContent := b.String()
718 helpView := helpStyle.Render("↑/↓: navigate • enter: select/toggle • esc: back")
719
720 if m.height > 0 {
721 currentHeight := lipgloss.Height(docStyle.Render(mainContent + helpView))
722 gap := m.height - currentHeight
723 if gap > 0 {
724 mainContent += strings.Repeat("\n", gap)
725 }
726 } else {
727 mainContent += "\n\n"
728 }
729
730 return docStyle.Render(mainContent + helpView)
731}
732
733func (m *Settings) viewAccounts() string {
734 var b strings.Builder
735
736 b.WriteString(titleStyle.Render("Account Settings"))
737 b.WriteString("\n\n")
738
739 if len(m.cfg.Accounts) == 0 {
740 b.WriteString(accountEmailStyle.Render(" No accounts configured.\n"))
741 b.WriteString("\n")
742 }
743
744 for i, account := range m.cfg.Accounts {
745 displayName := account.Email
746 if account.Name != "" {
747 displayName = fmt.Sprintf("%s (%s)", account.Name, account.FetchEmail)
748 }
749
750 providerInfo := account.ServiceProvider
751 if account.ServiceProvider == "custom" {
752 providerInfo = fmt.Sprintf("custom: %s", account.IMAPServer)
753 }
754
755 if account.SMIMECert != "" && account.SMIMEKey != "" {
756 providerInfo += " [S/MIME Configured]"
757 }
758 if account.PGPPublicKey != "" && account.PGPPrivateKey != "" {
759 providerInfo += " [PGP Configured]"
760 }
761
762 line := fmt.Sprintf("%s - %s", displayName, accountEmailStyle.Render(providerInfo))
763
764 if m.cursor == i {
765 b.WriteString(selectedAccountItemStyle.Render(fmt.Sprintf("> %s", line)))
766 } else {
767 b.WriteString(accountItemStyle.Render(fmt.Sprintf(" %s", line)))
768 }
769 b.WriteString("\n")
770 }
771
772 // Add Account option
773 addAccountText := "Add New Account"
774 if m.cursor == len(m.cfg.Accounts) {
775 b.WriteString(selectedAccountItemStyle.Render(fmt.Sprintf("> %s", addAccountText)))
776 } else {
777 b.WriteString(accountItemStyle.Render(fmt.Sprintf(" %s", addAccountText)))
778 }
779 b.WriteString("\n")
780
781 mainContent := b.String()
782 helpView := helpStyle.Render("↑/↓: navigate • enter: select • e: edit • d: delete • esc: back")
783
784 if m.height > 0 {
785 currentHeight := lipgloss.Height(docStyle.Render(mainContent + helpView))
786 gap := m.height - currentHeight
787 if gap > 0 {
788 mainContent += strings.Repeat("\n", gap)
789 }
790 } else {
791 mainContent += "\n\n"
792 }
793
794 if m.confirmingDelete {
795 accountName := m.cfg.Accounts[m.cursor].Email
796 dialog := DialogBoxStyle.Render(
797 lipgloss.JoinVertical(lipgloss.Center,
798 dangerStyle.Render("Delete account?"),
799 accountEmailStyle.Render(accountName),
800 HelpStyle.Render("\n(y/n)"),
801 ),
802 )
803 return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, dialog)
804 }
805
806 return docStyle.Render(mainContent + helpView)
807}
808
809func (m *Settings) viewSMIMEConfig() string {
810 var b strings.Builder
811
812 account := m.cfg.Accounts[m.editingAccountIdx]
813 b.WriteString(titleStyle.Render(fmt.Sprintf("Crypto Configuration for %s", account.FetchEmail)))
814 b.WriteString("\n\n")
815
816 // --- S/MIME Section ---
817 b.WriteString(settingsFocusedStyle.Render("S/MIME") + "\n")
818
819 if m.focusIndex == 0 {
820 b.WriteString(settingsFocusedStyle.Render("Certificate (PEM) Path:\n"))
821 } else {
822 b.WriteString(settingsBlurredStyle.Render("Certificate (PEM) Path:\n"))
823 }
824 b.WriteString(m.smimeCertInput.View() + "\n\n")
825
826 if m.focusIndex == 1 {
827 b.WriteString(settingsFocusedStyle.Render("Private Key (PEM) Path:\n"))
828 } else {
829 b.WriteString(settingsBlurredStyle.Render("Private Key (PEM) Path:\n"))
830 }
831 b.WriteString(m.smimeKeyInput.View() + "\n\n")
832
833 smimeSignStatus := "OFF"
834 if account.SMIMESignByDefault {
835 smimeSignStatus = "ON"
836 }
837 if m.focusIndex == 2 {
838 b.WriteString(settingsFocusedStyle.Render(fmt.Sprintf("> Sign By Default: %s\n\n", smimeSignStatus)))
839 } else {
840 b.WriteString(settingsBlurredStyle.Render(fmt.Sprintf(" Sign By Default: %s\n\n", smimeSignStatus)))
841 }
842
843 // --- PGP Section ---
844 b.WriteString(settingsFocusedStyle.Render("PGP") + "\n")
845
846 if m.focusIndex == 3 {
847 b.WriteString(settingsFocusedStyle.Render("Public Key Path:\n"))
848 } else {
849 b.WriteString(settingsBlurredStyle.Render("Public Key Path:\n"))
850 }
851 b.WriteString(m.pgpPublicKeyInput.View() + "\n\n")
852
853 if m.focusIndex == 4 {
854 b.WriteString(settingsFocusedStyle.Render("Private Key Path:\n"))
855 } else {
856 b.WriteString(settingsBlurredStyle.Render("Private Key Path:\n"))
857 }
858 b.WriteString(m.pgpPrivateKeyInput.View() + "\n\n")
859
860 // Key source toggle
861 keySourceDisplay := "File"
862 if m.pgpKeySource == "yubikey" {
863 keySourceDisplay = "YubiKey"
864 }
865 if m.focusIndex == 5 {
866 b.WriteString(settingsFocusedStyle.Render(fmt.Sprintf("> Key Source: %s\n\n", keySourceDisplay)))
867 } else {
868 b.WriteString(settingsBlurredStyle.Render(fmt.Sprintf(" Key Source: %s\n\n", keySourceDisplay)))
869 }
870
871 // PIN input (only shown if YubiKey is selected)
872 if m.pgpKeySource == "yubikey" {
873 if m.focusIndex == 6 {
874 b.WriteString(settingsFocusedStyle.Render("YubiKey PIN:\n"))
875 } else {
876 b.WriteString(settingsBlurredStyle.Render("YubiKey PIN:\n"))
877 }
878 b.WriteString(m.pgpPINInput.View() + "\n\n")
879 }
880
881 pgpSignStatus := "OFF"
882 if account.PGPSignByDefault {
883 pgpSignStatus = "ON"
884 }
885 if m.focusIndex == 7 {
886 b.WriteString(settingsFocusedStyle.Render(fmt.Sprintf("> Sign By Default: %s\n\n", pgpSignStatus)))
887 } else {
888 b.WriteString(settingsBlurredStyle.Render(fmt.Sprintf(" Sign By Default: %s\n\n", pgpSignStatus)))
889 }
890
891 // --- Buttons ---
892 saveBtn := "[ Save ]"
893 cancelBtn := "[ Cancel ]"
894
895 if m.focusIndex == 8 {
896 saveBtn = settingsFocusedStyle.Render(saveBtn)
897 } else {
898 saveBtn = settingsBlurredStyle.Render(saveBtn)
899 }
900
901 if m.focusIndex == 9 {
902 cancelBtn = settingsFocusedStyle.Render(cancelBtn)
903 } else {
904 cancelBtn = settingsBlurredStyle.Render(cancelBtn)
905 }
906
907 b.WriteString(saveBtn + " " + cancelBtn + "\n\n")
908
909 mainContent := b.String()
910 helpView := helpStyle.Render("tab/shift+tab: navigate • enter: save/next • space: toggle • esc: back")
911
912 if m.height > 0 {
913 currentHeight := lipgloss.Height(docStyle.Render(mainContent + helpView))
914 gap := m.height - currentHeight
915 if gap > 0 {
916 mainContent += strings.Repeat("\n", gap)
917 }
918 } else {
919 mainContent += "\n\n"
920 }
921
922 return docStyle.Render(mainContent + helpView)
923}
924
925func (m *Settings) viewMailingLists() string {
926 var b strings.Builder
927
928 b.WriteString(titleStyle.Render("Mailing Lists"))
929 b.WriteString("\n\n")
930
931 if len(m.cfg.MailingLists) == 0 {
932 b.WriteString(accountEmailStyle.Render(" No mailing lists configured.\n"))
933 b.WriteString("\n")
934 }
935
936 for i, list := range m.cfg.MailingLists {
937 displayName := list.Name
938
939 addrCount := fmt.Sprintf("%d address", len(list.Addresses))
940 if len(list.Addresses) != 1 {
941 addrCount += "es"
942 }
943
944 line := fmt.Sprintf("%s - %s", displayName, accountEmailStyle.Render(addrCount))
945
946 if m.cursor == i {
947 b.WriteString(selectedAccountItemStyle.Render(fmt.Sprintf("> %s", line)))
948 } else {
949 b.WriteString(accountItemStyle.Render(fmt.Sprintf(" %s", line)))
950 }
951 b.WriteString("\n")
952 }
953
954 // Add Mailing List option
955 addListText := "Add New Mailing List"
956 if m.cursor == len(m.cfg.MailingLists) {
957 b.WriteString(selectedAccountItemStyle.Render(fmt.Sprintf("> %s", addListText)))
958 } else {
959 b.WriteString(accountItemStyle.Render(fmt.Sprintf(" %s", addListText)))
960 }
961 b.WriteString("\n")
962
963 helpView := helpStyle.Render("↑/↓: navigate • enter: select • e: edit • d: delete • esc: back")
964 mainContent := b.String()
965
966 if m.height > 0 {
967 contentHeight := strings.Count(mainContent, "\n") + 1
968 gap := m.height - contentHeight - 2 // -2 for margins
969 if gap > 0 {
970 mainContent += strings.Repeat("\n", gap)
971 }
972 } else {
973 mainContent += "\n\n"
974 }
975
976 if m.confirmingDelete {
977 listName := m.cfg.MailingLists[m.cursor].Name
978 dialog := DialogBoxStyle.Render(
979 lipgloss.JoinVertical(lipgloss.Center,
980 dangerStyle.Render("Delete mailing list?"),
981 accountEmailStyle.Render(listName),
982 HelpStyle.Render("\n(y/n)"),
983 ),
984 )
985 return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, dialog)
986 }
987
988 return docStyle.Render(mainContent + helpView)
989}
990
991func (m *Settings) updateTheme(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
992 themes := theme.AllThemes()
993
994 switch msg.String() {
995 case "up", "k":
996 if m.cursor > 0 {
997 m.cursor--
998 }
999 case "down", "j":
1000 if m.cursor < len(themes)-1 {
1001 m.cursor++
1002 }
1003 case "enter":
1004 if m.cursor < len(themes) {
1005 selected := themes[m.cursor]
1006 theme.SetTheme(selected.Name)
1007 RebuildStyles()
1008 m.cfg.Theme = selected.Name
1009 _ = config.SaveConfig(m.cfg)
1010 }
1011 m.state = SettingsMain
1012 m.cursor = 1 // Return to Theme option
1013 return m, nil
1014 case "esc":
1015 m.state = SettingsMain
1016 m.cursor = 1 // Return to Theme option
1017 return m, nil
1018 }
1019 return m, nil
1020}
1021
1022func (m *Settings) viewTheme() string {
1023 themes := theme.AllThemes()
1024
1025 // Build left panel: theme list
1026 var left strings.Builder
1027 left.WriteString(titleStyle.Render("Theme") + "\n\n")
1028
1029 for i, t := range themes {
1030 isActive := t.Name == theme.ActiveTheme.Name
1031
1032 label := t.Name
1033 if isActive {
1034 label += " (active)"
1035 }
1036
1037 if m.cursor == i {
1038 left.WriteString(selectedAccountItemStyle.Render(fmt.Sprintf("> %s", label)))
1039 } else {
1040 left.WriteString(accountItemStyle.Render(fmt.Sprintf(" %s", label)))
1041 }
1042 left.WriteString("\n")
1043 }
1044
1045 left.WriteString("\n")
1046 if !m.cfg.HideTips {
1047 left.WriteString(TipStyle.Render("Tip: Custom themes can be added as\nJSON files in ~/.config/matcha/themes/") + "\n")
1048 }
1049
1050 // Build right panel: theme preview
1051 var previewTheme theme.Theme
1052 if m.cursor < len(themes) {
1053 previewTheme = themes[m.cursor]
1054 } else {
1055 previewTheme = theme.ActiveTheme
1056 }
1057 preview := renderThemePreview(previewTheme, m.width)
1058
1059 // Join panels side by side
1060 leftPanel := lipgloss.NewStyle().Width(30).Render(left.String())
1061 content := lipgloss.JoinHorizontal(lipgloss.Top, leftPanel, " ", preview)
1062
1063 helpView := helpStyle.Render("↑/↓: navigate • enter: select • esc: back")
1064
1065 if m.height > 0 {
1066 currentHeight := lipgloss.Height(docStyle.Render(content + "\n" + helpView))
1067 gap := m.height - currentHeight
1068 if gap > 0 {
1069 content += strings.Repeat("\n", gap)
1070 }
1071 } else {
1072 content += "\n\n"
1073 }
1074
1075 return docStyle.Render(content + "\n" + helpView)
1076}
1077
1078// renderThemePreview renders a small mockup showing how a theme looks.
1079func renderThemePreview(t theme.Theme, maxWidth int) string {
1080 previewWidth := maxWidth - 38 // 30 for left panel + padding/margins
1081 if previewWidth < 30 {
1082 previewWidth = 30
1083 }
1084 if previewWidth > 60 {
1085 previewWidth = 60
1086 }
1087
1088 accent := lipgloss.NewStyle().Foreground(t.Accent)
1089 accentBold := lipgloss.NewStyle().Foreground(t.Accent).Bold(true)
1090 secondary := lipgloss.NewStyle().Foreground(t.Secondary)
1091 muted := lipgloss.NewStyle().Foreground(t.MutedText)
1092 dim := lipgloss.NewStyle().Foreground(t.DimText)
1093 danger := lipgloss.NewStyle().Foreground(t.Danger)
1094 warn := lipgloss.NewStyle().Foreground(t.Warning)
1095 tip := lipgloss.NewStyle().Foreground(t.Tip).Italic(true)
1096 link := lipgloss.NewStyle().Foreground(t.Link)
1097 title := lipgloss.NewStyle().Foreground(t.AccentText).Background(t.AccentDark).Padding(0, 1)
1098 activeTab := lipgloss.NewStyle().Foreground(t.Accent).Bold(true).Underline(true)
1099 activeFolder := lipgloss.NewStyle().Background(t.Accent).Foreground(t.Contrast).Bold(true).Padding(0, 1)
1100
1101 var b strings.Builder
1102
1103 b.WriteString(title.Render("Preview: "+t.Name) + "\n\n")
1104
1105 // Fake inbox tabs
1106 b.WriteString(activeTab.Render("Inbox") + " " + secondary.Render("Sent") + " " + secondary.Render("Drafts") + "\n")
1107 b.WriteString(secondary.Render(strings.Repeat("─", previewWidth)) + "\n")
1108
1109 // Fake email list
1110 b.WriteString(accentBold.Render("> ") + dim.Render("Alice ") + accent.Render("Meeting tomorrow") + " " + muted.Render("2m ago") + "\n")
1111 b.WriteString(" " + dim.Render("Bob ") + secondary.Render("Re: Project update") + " " + muted.Render("1h ago") + "\n")
1112 b.WriteString(" " + dim.Render("Carol ") + secondary.Render("Quick question") + " " + muted.Render("3h ago") + "\n\n")
1113
1114 // Folder sidebar sample
1115 b.WriteString(accentBold.Render("Folders") + "\n")
1116 b.WriteString(activeFolder.Render(" INBOX ") + " " + secondary.Render("Sent") + " " + secondary.Render("Trash") + "\n\n")
1117
1118 // Status indicators
1119 b.WriteString(accentBold.Render("Success: ") + accent.Render("Email sent!") + "\n")
1120 b.WriteString(danger.Render("Error: ") + danger.Render("Connection failed") + "\n")
1121 b.WriteString(warn.Render("Update available: v2.0") + "\n")
1122 b.WriteString(tip.Render("Tip: Press ? for help") + "\n")
1123 b.WriteString(link.Render("https://example.com") + "\n")
1124
1125 box := lipgloss.NewStyle().
1126 Border(lipgloss.RoundedBorder()).
1127 BorderForeground(t.AccentDark).
1128 Padding(1, 2).
1129 Width(previewWidth).
1130 Render(b.String())
1131
1132 return box
1133}
1134
1135func (m *Settings) updateEncryption(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
1136 isEnabled := config.IsSecureModeEnabled()
1137
1138 if isEnabled {
1139 // Disable flow: confirmation dialog
1140 if m.confirmingDisable {
1141 switch msg.String() {
1142 case "y", "Y":
1143 m.confirmingDisable = false
1144 cfg := m.cfg
1145 return m, func() tea.Msg {
1146 err := config.DisableSecureMode(cfg)
1147 return SecureModeDisabledMsg{Err: err}
1148 }
1149 case "n", "N", "esc":
1150 m.confirmingDisable = false
1151 m.state = SettingsMain
1152 m.cursor = 7
1153 return m, nil
1154 }
1155 return m, nil
1156 }
1157
1158 // Show disable prompt
1159 m.confirmingDisable = true
1160 return m, nil
1161 }
1162
1163 // Enable flow: password + confirm
1164 switch msg.String() {
1165 case "esc":
1166 m.state = SettingsMain
1167 m.cursor = 7
1168 return m, nil
1169 case "tab", "shift+tab", "down", "up":
1170 if msg.String() == "shift+tab" || msg.String() == "up" {
1171 m.encFocusIndex--
1172 if m.encFocusIndex < 0 {
1173 m.encFocusIndex = 2 // wrap to cancel
1174 }
1175 } else {
1176 m.encFocusIndex++
1177 if m.encFocusIndex > 2 {
1178 m.encFocusIndex = 0
1179 }
1180 }
1181 m.encPasswordInput.Blur()
1182 m.encConfirmInput.Blur()
1183 var cmds []tea.Cmd
1184 switch m.encFocusIndex {
1185 case 0:
1186 cmds = append(cmds, m.encPasswordInput.Focus())
1187 case 1:
1188 cmds = append(cmds, m.encConfirmInput.Focus())
1189 }
1190 return m, tea.Batch(cmds...)
1191 case "enter":
1192 switch m.encFocusIndex {
1193 case 0: // Password field — advance to confirm
1194 m.encFocusIndex = 1
1195 m.encPasswordInput.Blur()
1196 return m, m.encConfirmInput.Focus()
1197 case 1: // Confirm field — advance to save
1198 m.encFocusIndex = 2
1199 m.encConfirmInput.Blur()
1200 return m, nil
1201 case 2: // Save button
1202 password := m.encPasswordInput.Value()
1203 confirm := m.encConfirmInput.Value()
1204 if password == "" {
1205 m.encError = "Password cannot be empty"
1206 return m, nil
1207 }
1208 if password != confirm {
1209 m.encError = "Passwords do not match"
1210 return m, nil
1211 }
1212 m.encEnabling = true
1213 m.encError = ""
1214 cfg := m.cfg
1215 return m, func() tea.Msg {
1216 err := config.EnableSecureMode(password, cfg)
1217 return SecureModeEnabledMsg{Err: err}
1218 }
1219 }
1220 }
1221
1222 // Update text inputs
1223 var cmd tea.Cmd
1224 switch m.encFocusIndex {
1225 case 0:
1226 m.encPasswordInput, cmd = m.encPasswordInput.Update(msg)
1227 case 1:
1228 m.encConfirmInput, cmd = m.encConfirmInput.Update(msg)
1229 }
1230 return m, cmd
1231}
1232
1233func (m *Settings) viewEncryption() string {
1234 var b strings.Builder
1235
1236 isEnabled := config.IsSecureModeEnabled()
1237
1238 b.WriteString(titleStyle.Render("Encryption") + "\n\n")
1239
1240 if isEnabled {
1241 // Disable mode
1242 if m.confirmingDisable {
1243 dialog := DialogBoxStyle.Render(
1244 lipgloss.JoinVertical(lipgloss.Center,
1245 dangerStyle.Render("Disable encryption?"),
1246 accountEmailStyle.Render("All data will be stored unencrypted."),
1247 HelpStyle.Render("\n(y/n)"),
1248 ),
1249 )
1250 return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, dialog)
1251 }
1252
1253 b.WriteString(settingsFocusedStyle.Render(" Encryption is currently enabled.") + "\n\n")
1254 b.WriteString(accountEmailStyle.Render(" Press enter to disable encryption.") + "\n")
1255 } else {
1256 // Enable mode
1257 b.WriteString(accountEmailStyle.Render(" Set a password to encrypt all data.") + "\n\n")
1258
1259 if m.encFocusIndex == 0 {
1260 b.WriteString(settingsFocusedStyle.Render("Password:\n"))
1261 } else {
1262 b.WriteString(settingsBlurredStyle.Render("Password:\n"))
1263 }
1264 b.WriteString(m.encPasswordInput.View() + "\n\n")
1265
1266 if m.encFocusIndex == 1 {
1267 b.WriteString(settingsFocusedStyle.Render("Confirm Password:\n"))
1268 } else {
1269 b.WriteString(settingsBlurredStyle.Render("Confirm Password:\n"))
1270 }
1271 b.WriteString(m.encConfirmInput.View() + "\n\n")
1272
1273 saveBtn := "[ Enable Encryption ]"
1274 if m.encFocusIndex == 2 {
1275 saveBtn = settingsFocusedStyle.Render(saveBtn)
1276 } else {
1277 saveBtn = settingsBlurredStyle.Render(saveBtn)
1278 }
1279 b.WriteString(saveBtn + "\n")
1280
1281 if m.encEnabling {
1282 b.WriteString("\n" + accountEmailStyle.Render(" Encrypting data...") + "\n")
1283 }
1284 }
1285
1286 if m.encError != "" {
1287 b.WriteString("\n" + dangerStyle.Render(" "+m.encError) + "\n")
1288 }
1289
1290 mainContent := b.String()
1291 helpView := helpStyle.Render("tab/shift+tab: navigate • enter: select • esc: back")
1292
1293 if m.height > 0 {
1294 currentHeight := lipgloss.Height(docStyle.Render(mainContent + helpView))
1295 gap := m.height - currentHeight
1296 if gap > 0 {
1297 mainContent += strings.Repeat("\n", gap)
1298 }
1299 } else {
1300 mainContent += "\n\n"
1301 }
1302
1303 return docStyle.Render(mainContent + helpView)
1304}
1305
1306// UpdateConfig updates the configuration (used when accounts are deleted).
1307func (m *Settings) UpdateConfig(cfg *config.Config) {
1308 m.cfg = cfg
1309 if m.state == SettingsAccounts && m.cursor >= len(cfg.Accounts) {
1310 m.cursor = len(cfg.Accounts)
1311 }
1312}