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 IMAPServer: acc.IMAPServer,
332 IMAPPort: acc.IMAPPort,
333 SMTPServer: acc.SMTPServer,
334 SMTPPort: acc.SMTPPort,
335 Protocol: acc.Protocol,
336 JMAPEndpoint: acc.JMAPEndpoint,
337 POP3Server: acc.POP3Server,
338 POP3Port: acc.POP3Port,
339 }
340 }
341 }
342 case "enter":
343 // If cursor is on "Add Account"
344 if m.cursor == len(m.cfg.Accounts) {
345 return m, func() tea.Msg { return GoToAddAccountMsg{} }
346 } else if m.cursor < len(m.cfg.Accounts) {
347 m.editingAccountIdx = m.cursor
348 m.state = SettingsSMIMEConfig
349 m.smimeCertInput.SetValue(m.cfg.Accounts[m.cursor].SMIMECert)
350 m.smimeKeyInput.SetValue(m.cfg.Accounts[m.cursor].SMIMEKey)
351 m.pgpPublicKeyInput.SetValue(m.cfg.Accounts[m.cursor].PGPPublicKey)
352 m.pgpPrivateKeyInput.SetValue(m.cfg.Accounts[m.cursor].PGPPrivateKey)
353 // Initialize PGP key source
354 if m.cfg.Accounts[m.cursor].PGPKeySource == "" {
355 m.pgpKeySource = "file"
356 } else {
357 m.pgpKeySource = m.cfg.Accounts[m.cursor].PGPKeySource
358 }
359 m.pgpPINInput.SetValue(m.cfg.Accounts[m.cursor].PGPPIN)
360 m.focusIndex = 0
361 m.smimeCertInput.Focus()
362 m.smimeKeyInput.Blur()
363 m.pgpPublicKeyInput.Blur()
364 m.pgpPrivateKeyInput.Blur()
365 m.pgpPINInput.Blur()
366 return m, textinput.Blink
367 }
368 case "esc":
369 m.state = SettingsMain
370 m.cursor = 0
371 return m, nil
372 }
373 return m, nil
374}
375
376// Focus indices for the crypto config screen:
377// 0: S/MIME Certificate Path
378// 1: S/MIME Private Key Path
379// 2: S/MIME Sign By Default toggle
380// 3: PGP Public Key Path
381// 4: PGP Private Key Path
382// 5: PGP Key Source toggle (file/yubikey)
383// 6: PGP PIN input (only shown if yubikey)
384// 7: PGP Sign By Default toggle
385// 8: Save button
386// 9: Cancel button
387const cryptoConfigMaxFocus = 9
388
389func (m *Settings) updateSMIMEConfig(msg tea.KeyPressMsg) (*Settings, tea.Cmd) {
390 var cmds []tea.Cmd
391 var cmd tea.Cmd
392
393 switch msg.String() {
394 case "esc":
395 m.state = SettingsAccounts
396 return m, nil
397 case "tab", "shift+tab", "up", "down":
398 if msg.String() == "shift+tab" || msg.String() == "up" {
399 m.focusIndex--
400 if m.focusIndex < 0 {
401 m.focusIndex = cryptoConfigMaxFocus
402 }
403 } else {
404 m.focusIndex++
405 if m.focusIndex > cryptoConfigMaxFocus {
406 m.focusIndex = 0
407 }
408 }
409
410 m.smimeCertInput.Blur()
411 m.smimeKeyInput.Blur()
412 m.pgpPublicKeyInput.Blur()
413 m.pgpPrivateKeyInput.Blur()
414 m.pgpPINInput.Blur()
415
416 switch m.focusIndex {
417 case 0:
418 cmds = append(cmds, m.smimeCertInput.Focus())
419 case 1:
420 cmds = append(cmds, m.smimeKeyInput.Focus())
421 case 3:
422 cmds = append(cmds, m.pgpPublicKeyInput.Focus())
423 case 4:
424 cmds = append(cmds, m.pgpPrivateKeyInput.Focus())
425 case 6:
426 cmds = append(cmds, m.pgpPINInput.Focus())
427 }
428 return m, tea.Batch(cmds...)
429 case "enter", " ":
430 switch m.focusIndex {
431 case 0: // S/MIME cert - enter advances to next field
432 if msg.String() == "enter" {
433 m.focusIndex = 1
434 m.smimeCertInput.Blur()
435 cmds = append(cmds, m.smimeKeyInput.Focus())
436 return m, tea.Batch(cmds...)
437 }
438 case 1: // S/MIME key - enter advances
439 if msg.String() == "enter" {
440 m.focusIndex = 2
441 m.smimeKeyInput.Blur()
442 return m, nil
443 }
444 case 2: // S/MIME sign toggle
445 if msg.String() == "enter" || msg.String() == " " {
446 m.cfg.Accounts[m.editingAccountIdx].SMIMESignByDefault = !m.cfg.Accounts[m.editingAccountIdx].SMIMESignByDefault
447 }
448 return m, nil
449 case 3: // PGP public key - enter advances
450 if msg.String() == "enter" {
451 m.focusIndex = 4
452 m.pgpPublicKeyInput.Blur()
453 cmds = append(cmds, m.pgpPrivateKeyInput.Focus())
454 return m, tea.Batch(cmds...)
455 }
456 case 4: // PGP private key - enter advances
457 if msg.String() == "enter" {
458 m.focusIndex = 5
459 m.pgpPrivateKeyInput.Blur()
460 return m, nil
461 }
462 case 5: // PGP key source toggle (file/yubikey)
463 if msg.String() == "enter" || msg.String() == " " {
464 if m.pgpKeySource == "file" {
465 m.pgpKeySource = "yubikey"
466 } else {
467 m.pgpKeySource = "file"
468 }
469 }
470 return m, nil
471 case 6: // PGP PIN input - enter advances
472 if msg.String() == "enter" {
473 m.focusIndex = 7
474 m.pgpPINInput.Blur()
475 return m, nil
476 }
477 case 7: // PGP sign toggle
478 if msg.String() == "enter" || msg.String() == " " {
479 m.cfg.Accounts[m.editingAccountIdx].PGPSignByDefault = !m.cfg.Accounts[m.editingAccountIdx].PGPSignByDefault
480 }
481 return m, nil
482 case 8: // Save
483 if msg.String() == "enter" {
484 m.cfg.Accounts[m.editingAccountIdx].SMIMECert = m.smimeCertInput.Value()
485 m.cfg.Accounts[m.editingAccountIdx].SMIMEKey = m.smimeKeyInput.Value()
486 m.cfg.Accounts[m.editingAccountIdx].PGPPublicKey = m.pgpPublicKeyInput.Value()
487 m.cfg.Accounts[m.editingAccountIdx].PGPPrivateKey = m.pgpPrivateKeyInput.Value()
488 m.cfg.Accounts[m.editingAccountIdx].PGPKeySource = m.pgpKeySource
489 m.cfg.Accounts[m.editingAccountIdx].PGPPIN = m.pgpPINInput.Value()
490 _ = config.SaveConfig(m.cfg)
491 m.state = SettingsAccounts
492 return m, nil
493 }
494 case 9: // Cancel
495 if msg.String() == "enter" {
496 m.state = SettingsAccounts
497 return m, nil
498 }
499 }
500 }
501
502 switch m.focusIndex {
503 case 0:
504 m.smimeCertInput, cmd = m.smimeCertInput.Update(msg)
505 cmds = append(cmds, cmd)
506 case 1:
507 m.smimeKeyInput, cmd = m.smimeKeyInput.Update(msg)
508 cmds = append(cmds, cmd)
509 case 3:
510 m.pgpPublicKeyInput, cmd = m.pgpPublicKeyInput.Update(msg)
511 cmds = append(cmds, cmd)
512 case 4:
513 m.pgpPrivateKeyInput, cmd = m.pgpPrivateKeyInput.Update(msg)
514 cmds = append(cmds, cmd)
515 case 6:
516 m.pgpPINInput, cmd = m.pgpPINInput.Update(msg)
517 cmds = append(cmds, cmd)
518 }
519
520 return m, tea.Batch(cmds...)
521}
522
523func (m *Settings) updateMailingLists(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
524 if m.confirmingDelete {
525 switch msg.String() {
526 case "y", "Y":
527 if m.cursor < len(m.cfg.MailingLists) {
528 m.cfg.MailingLists = append(m.cfg.MailingLists[:m.cursor], m.cfg.MailingLists[m.cursor+1:]...)
529 _ = config.SaveConfig(m.cfg)
530 if m.cursor >= len(m.cfg.MailingLists) && m.cursor > 0 {
531 m.cursor--
532 }
533 m.confirmingDelete = false
534 }
535 case "n", "N", "esc":
536 m.confirmingDelete = false
537 return m, nil
538 }
539 return m, nil
540 }
541
542 switch msg.String() {
543 case "up", "k":
544 if m.cursor > 0 {
545 m.cursor--
546 }
547 case "down", "j":
548 if m.cursor < len(m.cfg.MailingLists) {
549 m.cursor++
550 }
551 case "d":
552 if m.cursor < len(m.cfg.MailingLists) && len(m.cfg.MailingLists) > 0 {
553 m.confirmingDelete = true
554 }
555 case "e":
556 // Edit selected mailing list
557 if m.cursor < len(m.cfg.MailingLists) {
558 list := m.cfg.MailingLists[m.cursor]
559 idx := m.cursor
560 return m, func() tea.Msg {
561 return GoToEditMailingListMsg{
562 Index: idx,
563 Name: list.Name,
564 Addresses: strings.Join(list.Addresses, ", "),
565 }
566 }
567 }
568 case "enter":
569 if m.cursor == len(m.cfg.MailingLists) {
570 return m, func() tea.Msg { return GoToAddMailingListMsg{} }
571 }
572 case "esc":
573 m.state = SettingsMain
574 m.cursor = 0
575 return m, nil
576 }
577 return m, nil
578}
579
580// View renders the settings screen.
581func (m *Settings) View() tea.View {
582 if m.state == SettingsMain {
583 return tea.NewView(m.viewMain())
584 } else if m.state == SettingsTheme {
585 return tea.NewView(m.viewTheme())
586 } else if m.state == SettingsMailingLists {
587 return tea.NewView(m.viewMailingLists())
588 } else if m.state == SettingsSMIMEConfig {
589 return tea.NewView(m.viewSMIMEConfig())
590 } else if m.state == SettingsEncryption {
591 return tea.NewView(m.viewEncryption())
592 }
593 return tea.NewView(m.viewAccounts())
594}
595
596func (m *Settings) viewMain() string {
597 var b strings.Builder
598
599 b.WriteString(titleStyle.Render("Settings") + "\n\n")
600
601 // Option 0: Email Accounts
602 if m.cursor == 0 {
603 b.WriteString(selectedAccountItemStyle.Render("> Email Accounts"))
604 } else {
605 b.WriteString(accountItemStyle.Render(" Email Accounts"))
606 }
607 b.WriteString("\n")
608
609 // Option 1: Theme
610 themeText := fmt.Sprintf("Theme: %s", theme.ActiveTheme.Name)
611 if m.cursor == 1 {
612 b.WriteString(selectedAccountItemStyle.Render("> " + themeText))
613 } else {
614 b.WriteString(accountItemStyle.Render(" " + themeText))
615 }
616 b.WriteString("\n")
617
618 // Option 2: Image Display
619 status := "ON"
620 if m.cfg.DisableImages {
621 status = "OFF"
622 }
623 text := fmt.Sprintf("Image Display: %s", status)
624 if m.cursor == 2 {
625 b.WriteString(selectedAccountItemStyle.Render("> " + text))
626 } else {
627 b.WriteString(accountItemStyle.Render(" " + text))
628 }
629 b.WriteString("\n")
630
631 // Option 3: Edit Signature
632 sigText := "Edit Signature"
633 if config.HasSignature() {
634 sigText = "Edit Signature (configured)"
635 }
636 if m.cursor == 3 {
637 b.WriteString(selectedAccountItemStyle.Render("> " + sigText))
638 } else {
639 b.WriteString(accountItemStyle.Render(" " + sigText))
640 }
641 b.WriteString("\n")
642
643 // Option 4: Contextual Tips
644 tipsStatus := "ON"
645 if m.cfg.HideTips {
646 tipsStatus = "OFF"
647 }
648 tipsText := fmt.Sprintf("Contextual Tips: %s", tipsStatus)
649 if m.cursor == 4 {
650 b.WriteString(selectedAccountItemStyle.Render("> " + tipsText))
651 } else {
652 b.WriteString(accountItemStyle.Render(" " + tipsText))
653 }
654 b.WriteString("\n")
655
656 // Option 5: Desktop Notifications
657 notifStatus := "ON"
658 if m.cfg.DisableNotifications {
659 notifStatus = "OFF"
660 }
661 notifText := fmt.Sprintf("Desktop Notifications: %s", notifStatus)
662 if m.cursor == 5 {
663 b.WriteString(selectedAccountItemStyle.Render("> " + notifText))
664 } else {
665 b.WriteString(accountItemStyle.Render(" " + notifText))
666 }
667 b.WriteString("\n")
668
669 // Option 6: Mailing Lists
670 mailingListsText := "Mailing Lists"
671 if m.cursor == 6 {
672 b.WriteString(selectedAccountItemStyle.Render("> " + mailingListsText))
673 } else {
674 b.WriteString(accountItemStyle.Render(" " + mailingListsText))
675 }
676 b.WriteString("\n")
677
678 // Option 7: Encryption
679 encStatus := "OFF"
680 if config.IsSecureModeEnabled() {
681 encStatus = "ON"
682 }
683 encText := fmt.Sprintf("Encryption: %s", encStatus)
684 if m.cursor == 7 {
685 b.WriteString(selectedAccountItemStyle.Render("> " + encText))
686 } else {
687 b.WriteString(accountItemStyle.Render(" " + encText))
688 }
689 b.WriteString("\n\n")
690
691 if !m.cfg.HideTips {
692 tip := ""
693 switch m.cursor {
694 case 0:
695 tip = "Manage your connected email accounts."
696 case 1:
697 tip = "Choose a color theme for the application."
698 case 2:
699 tip = "Toggle displaying images in emails."
700 case 3:
701 tip = "Configure the signature appended to your outgoing emails."
702 case 4:
703 tip = "Toggle displaying helpful contextual tips like this one."
704 case 5:
705 tip = "Toggle desktop notifications when new mail arrives."
706 case 6:
707 tip = "Manage groups of email addresses to quickly send to multiple people."
708 case 7:
709 tip = "Encrypt all data with a password. You'll need to enter it each time you open matcha."
710 }
711 if tip != "" {
712 b.WriteString(TipStyle.Render("Tip: "+tip) + "\n\n")
713 }
714 }
715
716 mainContent := b.String()
717 helpView := helpStyle.Render("↑/↓: navigate • enter: select/toggle • esc: back")
718
719 if m.height > 0 {
720 currentHeight := lipgloss.Height(docStyle.Render(mainContent + helpView))
721 gap := m.height - currentHeight
722 if gap > 0 {
723 mainContent += strings.Repeat("\n", gap)
724 }
725 } else {
726 mainContent += "\n\n"
727 }
728
729 return docStyle.Render(mainContent + helpView)
730}
731
732func (m *Settings) viewAccounts() string {
733 var b strings.Builder
734
735 b.WriteString(titleStyle.Render("Account Settings"))
736 b.WriteString("\n\n")
737
738 if len(m.cfg.Accounts) == 0 {
739 b.WriteString(accountEmailStyle.Render(" No accounts configured.\n"))
740 b.WriteString("\n")
741 }
742
743 for i, account := range m.cfg.Accounts {
744 displayName := account.Email
745 if account.Name != "" {
746 displayName = fmt.Sprintf("%s (%s)", account.Name, account.FetchEmail)
747 }
748
749 providerInfo := account.ServiceProvider
750 if account.ServiceProvider == "custom" {
751 providerInfo = fmt.Sprintf("custom: %s", account.IMAPServer)
752 }
753
754 if account.SMIMECert != "" && account.SMIMEKey != "" {
755 providerInfo += " [S/MIME Configured]"
756 }
757 if account.PGPPublicKey != "" && account.PGPPrivateKey != "" {
758 providerInfo += " [PGP Configured]"
759 }
760
761 line := fmt.Sprintf("%s - %s", displayName, accountEmailStyle.Render(providerInfo))
762
763 if m.cursor == i {
764 b.WriteString(selectedAccountItemStyle.Render(fmt.Sprintf("> %s", line)))
765 } else {
766 b.WriteString(accountItemStyle.Render(fmt.Sprintf(" %s", line)))
767 }
768 b.WriteString("\n")
769 }
770
771 // Add Account option
772 addAccountText := "Add New Account"
773 if m.cursor == len(m.cfg.Accounts) {
774 b.WriteString(selectedAccountItemStyle.Render(fmt.Sprintf("> %s", addAccountText)))
775 } else {
776 b.WriteString(accountItemStyle.Render(fmt.Sprintf(" %s", addAccountText)))
777 }
778 b.WriteString("\n")
779
780 mainContent := b.String()
781 helpView := helpStyle.Render("↑/↓: navigate • enter: select • e: edit • d: delete • esc: back")
782
783 if m.height > 0 {
784 currentHeight := lipgloss.Height(docStyle.Render(mainContent + helpView))
785 gap := m.height - currentHeight
786 if gap > 0 {
787 mainContent += strings.Repeat("\n", gap)
788 }
789 } else {
790 mainContent += "\n\n"
791 }
792
793 if m.confirmingDelete {
794 accountName := m.cfg.Accounts[m.cursor].Email
795 dialog := DialogBoxStyle.Render(
796 lipgloss.JoinVertical(lipgloss.Center,
797 dangerStyle.Render("Delete account?"),
798 accountEmailStyle.Render(accountName),
799 HelpStyle.Render("\n(y/n)"),
800 ),
801 )
802 return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, dialog)
803 }
804
805 return docStyle.Render(mainContent + helpView)
806}
807
808func (m *Settings) viewSMIMEConfig() string {
809 var b strings.Builder
810
811 account := m.cfg.Accounts[m.editingAccountIdx]
812 b.WriteString(titleStyle.Render(fmt.Sprintf("Crypto Configuration for %s", account.FetchEmail)))
813 b.WriteString("\n\n")
814
815 // --- S/MIME Section ---
816 b.WriteString(settingsFocusedStyle.Render("S/MIME") + "\n")
817
818 if m.focusIndex == 0 {
819 b.WriteString(settingsFocusedStyle.Render("Certificate (PEM) Path:\n"))
820 } else {
821 b.WriteString(settingsBlurredStyle.Render("Certificate (PEM) Path:\n"))
822 }
823 b.WriteString(m.smimeCertInput.View() + "\n\n")
824
825 if m.focusIndex == 1 {
826 b.WriteString(settingsFocusedStyle.Render("Private Key (PEM) Path:\n"))
827 } else {
828 b.WriteString(settingsBlurredStyle.Render("Private Key (PEM) Path:\n"))
829 }
830 b.WriteString(m.smimeKeyInput.View() + "\n\n")
831
832 smimeSignStatus := "OFF"
833 if account.SMIMESignByDefault {
834 smimeSignStatus = "ON"
835 }
836 if m.focusIndex == 2 {
837 b.WriteString(settingsFocusedStyle.Render(fmt.Sprintf("> Sign By Default: %s\n\n", smimeSignStatus)))
838 } else {
839 b.WriteString(settingsBlurredStyle.Render(fmt.Sprintf(" Sign By Default: %s\n\n", smimeSignStatus)))
840 }
841
842 // --- PGP Section ---
843 b.WriteString(settingsFocusedStyle.Render("PGP") + "\n")
844
845 if m.focusIndex == 3 {
846 b.WriteString(settingsFocusedStyle.Render("Public Key Path:\n"))
847 } else {
848 b.WriteString(settingsBlurredStyle.Render("Public Key Path:\n"))
849 }
850 b.WriteString(m.pgpPublicKeyInput.View() + "\n\n")
851
852 if m.focusIndex == 4 {
853 b.WriteString(settingsFocusedStyle.Render("Private Key Path:\n"))
854 } else {
855 b.WriteString(settingsBlurredStyle.Render("Private Key Path:\n"))
856 }
857 b.WriteString(m.pgpPrivateKeyInput.View() + "\n\n")
858
859 // Key source toggle
860 keySourceDisplay := "File"
861 if m.pgpKeySource == "yubikey" {
862 keySourceDisplay = "YubiKey"
863 }
864 if m.focusIndex == 5 {
865 b.WriteString(settingsFocusedStyle.Render(fmt.Sprintf("> Key Source: %s\n\n", keySourceDisplay)))
866 } else {
867 b.WriteString(settingsBlurredStyle.Render(fmt.Sprintf(" Key Source: %s\n\n", keySourceDisplay)))
868 }
869
870 // PIN input (only shown if YubiKey is selected)
871 if m.pgpKeySource == "yubikey" {
872 if m.focusIndex == 6 {
873 b.WriteString(settingsFocusedStyle.Render("YubiKey PIN:\n"))
874 } else {
875 b.WriteString(settingsBlurredStyle.Render("YubiKey PIN:\n"))
876 }
877 b.WriteString(m.pgpPINInput.View() + "\n\n")
878 }
879
880 pgpSignStatus := "OFF"
881 if account.PGPSignByDefault {
882 pgpSignStatus = "ON"
883 }
884 if m.focusIndex == 7 {
885 b.WriteString(settingsFocusedStyle.Render(fmt.Sprintf("> Sign By Default: %s\n\n", pgpSignStatus)))
886 } else {
887 b.WriteString(settingsBlurredStyle.Render(fmt.Sprintf(" Sign By Default: %s\n\n", pgpSignStatus)))
888 }
889
890 // --- Buttons ---
891 saveBtn := "[ Save ]"
892 cancelBtn := "[ Cancel ]"
893
894 if m.focusIndex == 8 {
895 saveBtn = settingsFocusedStyle.Render(saveBtn)
896 } else {
897 saveBtn = settingsBlurredStyle.Render(saveBtn)
898 }
899
900 if m.focusIndex == 9 {
901 cancelBtn = settingsFocusedStyle.Render(cancelBtn)
902 } else {
903 cancelBtn = settingsBlurredStyle.Render(cancelBtn)
904 }
905
906 b.WriteString(saveBtn + " " + cancelBtn + "\n\n")
907
908 mainContent := b.String()
909 helpView := helpStyle.Render("tab/shift+tab: navigate • enter: save/next • space: toggle • esc: back")
910
911 if m.height > 0 {
912 currentHeight := lipgloss.Height(docStyle.Render(mainContent + helpView))
913 gap := m.height - currentHeight
914 if gap > 0 {
915 mainContent += strings.Repeat("\n", gap)
916 }
917 } else {
918 mainContent += "\n\n"
919 }
920
921 return docStyle.Render(mainContent + helpView)
922}
923
924func (m *Settings) viewMailingLists() string {
925 var b strings.Builder
926
927 b.WriteString(titleStyle.Render("Mailing Lists"))
928 b.WriteString("\n\n")
929
930 if len(m.cfg.MailingLists) == 0 {
931 b.WriteString(accountEmailStyle.Render(" No mailing lists configured.\n"))
932 b.WriteString("\n")
933 }
934
935 for i, list := range m.cfg.MailingLists {
936 displayName := list.Name
937
938 addrCount := fmt.Sprintf("%d address", len(list.Addresses))
939 if len(list.Addresses) != 1 {
940 addrCount += "es"
941 }
942
943 line := fmt.Sprintf("%s - %s", displayName, accountEmailStyle.Render(addrCount))
944
945 if m.cursor == i {
946 b.WriteString(selectedAccountItemStyle.Render(fmt.Sprintf("> %s", line)))
947 } else {
948 b.WriteString(accountItemStyle.Render(fmt.Sprintf(" %s", line)))
949 }
950 b.WriteString("\n")
951 }
952
953 // Add Mailing List option
954 addListText := "Add New Mailing List"
955 if m.cursor == len(m.cfg.MailingLists) {
956 b.WriteString(selectedAccountItemStyle.Render(fmt.Sprintf("> %s", addListText)))
957 } else {
958 b.WriteString(accountItemStyle.Render(fmt.Sprintf(" %s", addListText)))
959 }
960 b.WriteString("\n")
961
962 helpView := helpStyle.Render("↑/↓: navigate • enter: select • e: edit • d: delete • esc: back")
963 mainContent := b.String()
964
965 if m.height > 0 {
966 contentHeight := strings.Count(mainContent, "\n") + 1
967 gap := m.height - contentHeight - 2 // -2 for margins
968 if gap > 0 {
969 mainContent += strings.Repeat("\n", gap)
970 }
971 } else {
972 mainContent += "\n\n"
973 }
974
975 if m.confirmingDelete {
976 listName := m.cfg.MailingLists[m.cursor].Name
977 dialog := DialogBoxStyle.Render(
978 lipgloss.JoinVertical(lipgloss.Center,
979 dangerStyle.Render("Delete mailing list?"),
980 accountEmailStyle.Render(listName),
981 HelpStyle.Render("\n(y/n)"),
982 ),
983 )
984 return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, dialog)
985 }
986
987 return docStyle.Render(mainContent + helpView)
988}
989
990func (m *Settings) updateTheme(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
991 themes := theme.AllThemes()
992
993 switch msg.String() {
994 case "up", "k":
995 if m.cursor > 0 {
996 m.cursor--
997 }
998 case "down", "j":
999 if m.cursor < len(themes)-1 {
1000 m.cursor++
1001 }
1002 case "enter":
1003 if m.cursor < len(themes) {
1004 selected := themes[m.cursor]
1005 theme.SetTheme(selected.Name)
1006 RebuildStyles()
1007 m.cfg.Theme = selected.Name
1008 _ = config.SaveConfig(m.cfg)
1009 }
1010 m.state = SettingsMain
1011 m.cursor = 1 // Return to Theme option
1012 return m, nil
1013 case "esc":
1014 m.state = SettingsMain
1015 m.cursor = 1 // Return to Theme option
1016 return m, nil
1017 }
1018 return m, nil
1019}
1020
1021func (m *Settings) viewTheme() string {
1022 themes := theme.AllThemes()
1023
1024 // Build left panel: theme list
1025 var left strings.Builder
1026 left.WriteString(titleStyle.Render("Theme") + "\n\n")
1027
1028 for i, t := range themes {
1029 isActive := t.Name == theme.ActiveTheme.Name
1030
1031 label := t.Name
1032 if isActive {
1033 label += " (active)"
1034 }
1035
1036 if m.cursor == i {
1037 left.WriteString(selectedAccountItemStyle.Render(fmt.Sprintf("> %s", label)))
1038 } else {
1039 left.WriteString(accountItemStyle.Render(fmt.Sprintf(" %s", label)))
1040 }
1041 left.WriteString("\n")
1042 }
1043
1044 left.WriteString("\n")
1045 if !m.cfg.HideTips {
1046 left.WriteString(TipStyle.Render("Tip: Custom themes can be added as\nJSON files in ~/.config/matcha/themes/") + "\n")
1047 }
1048
1049 // Build right panel: theme preview
1050 var previewTheme theme.Theme
1051 if m.cursor < len(themes) {
1052 previewTheme = themes[m.cursor]
1053 } else {
1054 previewTheme = theme.ActiveTheme
1055 }
1056 preview := renderThemePreview(previewTheme, m.width)
1057
1058 // Join panels side by side
1059 leftPanel := lipgloss.NewStyle().Width(30).Render(left.String())
1060 content := lipgloss.JoinHorizontal(lipgloss.Top, leftPanel, " ", preview)
1061
1062 helpView := helpStyle.Render("↑/↓: navigate • enter: select • esc: back")
1063
1064 if m.height > 0 {
1065 currentHeight := lipgloss.Height(docStyle.Render(content + "\n" + helpView))
1066 gap := m.height - currentHeight
1067 if gap > 0 {
1068 content += strings.Repeat("\n", gap)
1069 }
1070 } else {
1071 content += "\n\n"
1072 }
1073
1074 return docStyle.Render(content + "\n" + helpView)
1075}
1076
1077// renderThemePreview renders a small mockup showing how a theme looks.
1078func renderThemePreview(t theme.Theme, maxWidth int) string {
1079 previewWidth := maxWidth - 38 // 30 for left panel + padding/margins
1080 if previewWidth < 30 {
1081 previewWidth = 30
1082 }
1083 if previewWidth > 60 {
1084 previewWidth = 60
1085 }
1086
1087 accent := lipgloss.NewStyle().Foreground(t.Accent)
1088 accentBold := lipgloss.NewStyle().Foreground(t.Accent).Bold(true)
1089 secondary := lipgloss.NewStyle().Foreground(t.Secondary)
1090 muted := lipgloss.NewStyle().Foreground(t.MutedText)
1091 dim := lipgloss.NewStyle().Foreground(t.DimText)
1092 danger := lipgloss.NewStyle().Foreground(t.Danger)
1093 warn := lipgloss.NewStyle().Foreground(t.Warning)
1094 tip := lipgloss.NewStyle().Foreground(t.Tip).Italic(true)
1095 link := lipgloss.NewStyle().Foreground(t.Link)
1096 title := lipgloss.NewStyle().Foreground(t.AccentText).Background(t.AccentDark).Padding(0, 1)
1097 activeTab := lipgloss.NewStyle().Foreground(t.Accent).Bold(true).Underline(true)
1098 activeFolder := lipgloss.NewStyle().Background(t.Accent).Foreground(t.Contrast).Bold(true).Padding(0, 1)
1099
1100 var b strings.Builder
1101
1102 b.WriteString(title.Render("Preview: "+t.Name) + "\n\n")
1103
1104 // Fake inbox tabs
1105 b.WriteString(activeTab.Render("Inbox") + " " + secondary.Render("Sent") + " " + secondary.Render("Drafts") + "\n")
1106 b.WriteString(secondary.Render(strings.Repeat("─", previewWidth)) + "\n")
1107
1108 // Fake email list
1109 b.WriteString(accentBold.Render("> ") + dim.Render("Alice ") + accent.Render("Meeting tomorrow") + " " + muted.Render("2m ago") + "\n")
1110 b.WriteString(" " + dim.Render("Bob ") + secondary.Render("Re: Project update") + " " + muted.Render("1h ago") + "\n")
1111 b.WriteString(" " + dim.Render("Carol ") + secondary.Render("Quick question") + " " + muted.Render("3h ago") + "\n\n")
1112
1113 // Folder sidebar sample
1114 b.WriteString(accentBold.Render("Folders") + "\n")
1115 b.WriteString(activeFolder.Render(" INBOX ") + " " + secondary.Render("Sent") + " " + secondary.Render("Trash") + "\n\n")
1116
1117 // Status indicators
1118 b.WriteString(accentBold.Render("Success: ") + accent.Render("Email sent!") + "\n")
1119 b.WriteString(danger.Render("Error: ") + danger.Render("Connection failed") + "\n")
1120 b.WriteString(warn.Render("Update available: v2.0") + "\n")
1121 b.WriteString(tip.Render("Tip: Press ? for help") + "\n")
1122 b.WriteString(link.Render("https://example.com") + "\n")
1123
1124 box := lipgloss.NewStyle().
1125 Border(lipgloss.RoundedBorder()).
1126 BorderForeground(t.AccentDark).
1127 Padding(1, 2).
1128 Width(previewWidth).
1129 Render(b.String())
1130
1131 return box
1132}
1133
1134func (m *Settings) updateEncryption(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
1135 isEnabled := config.IsSecureModeEnabled()
1136
1137 if isEnabled {
1138 // Disable flow: confirmation dialog
1139 if m.confirmingDisable {
1140 switch msg.String() {
1141 case "y", "Y":
1142 m.confirmingDisable = false
1143 cfg := m.cfg
1144 return m, func() tea.Msg {
1145 err := config.DisableSecureMode(cfg)
1146 return SecureModeDisabledMsg{Err: err}
1147 }
1148 case "n", "N", "esc":
1149 m.confirmingDisable = false
1150 m.state = SettingsMain
1151 m.cursor = 7
1152 return m, nil
1153 }
1154 return m, nil
1155 }
1156
1157 // Show disable prompt
1158 m.confirmingDisable = true
1159 return m, nil
1160 }
1161
1162 // Enable flow: password + confirm
1163 switch msg.String() {
1164 case "esc":
1165 m.state = SettingsMain
1166 m.cursor = 7
1167 return m, nil
1168 case "tab", "shift+tab", "down", "up":
1169 if msg.String() == "shift+tab" || msg.String() == "up" {
1170 m.encFocusIndex--
1171 if m.encFocusIndex < 0 {
1172 m.encFocusIndex = 2 // wrap to cancel
1173 }
1174 } else {
1175 m.encFocusIndex++
1176 if m.encFocusIndex > 2 {
1177 m.encFocusIndex = 0
1178 }
1179 }
1180 m.encPasswordInput.Blur()
1181 m.encConfirmInput.Blur()
1182 var cmds []tea.Cmd
1183 switch m.encFocusIndex {
1184 case 0:
1185 cmds = append(cmds, m.encPasswordInput.Focus())
1186 case 1:
1187 cmds = append(cmds, m.encConfirmInput.Focus())
1188 }
1189 return m, tea.Batch(cmds...)
1190 case "enter":
1191 switch m.encFocusIndex {
1192 case 0: // Password field — advance to confirm
1193 m.encFocusIndex = 1
1194 m.encPasswordInput.Blur()
1195 return m, m.encConfirmInput.Focus()
1196 case 1: // Confirm field — advance to save
1197 m.encFocusIndex = 2
1198 m.encConfirmInput.Blur()
1199 return m, nil
1200 case 2: // Save button
1201 password := m.encPasswordInput.Value()
1202 confirm := m.encConfirmInput.Value()
1203 if password == "" {
1204 m.encError = "Password cannot be empty"
1205 return m, nil
1206 }
1207 if password != confirm {
1208 m.encError = "Passwords do not match"
1209 return m, nil
1210 }
1211 m.encEnabling = true
1212 m.encError = ""
1213 cfg := m.cfg
1214 return m, func() tea.Msg {
1215 err := config.EnableSecureMode(password, cfg)
1216 return SecureModeEnabledMsg{Err: err}
1217 }
1218 }
1219 }
1220
1221 // Update text inputs
1222 var cmd tea.Cmd
1223 switch m.encFocusIndex {
1224 case 0:
1225 m.encPasswordInput, cmd = m.encPasswordInput.Update(msg)
1226 case 1:
1227 m.encConfirmInput, cmd = m.encConfirmInput.Update(msg)
1228 }
1229 return m, cmd
1230}
1231
1232func (m *Settings) viewEncryption() string {
1233 var b strings.Builder
1234
1235 isEnabled := config.IsSecureModeEnabled()
1236
1237 b.WriteString(titleStyle.Render("Encryption") + "\n\n")
1238
1239 if isEnabled {
1240 // Disable mode
1241 if m.confirmingDisable {
1242 dialog := DialogBoxStyle.Render(
1243 lipgloss.JoinVertical(lipgloss.Center,
1244 dangerStyle.Render("Disable encryption?"),
1245 accountEmailStyle.Render("All data will be stored unencrypted."),
1246 HelpStyle.Render("\n(y/n)"),
1247 ),
1248 )
1249 return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, dialog)
1250 }
1251
1252 b.WriteString(settingsFocusedStyle.Render(" Encryption is currently enabled.") + "\n\n")
1253 b.WriteString(accountEmailStyle.Render(" Press enter to disable encryption.") + "\n")
1254 } else {
1255 // Enable mode
1256 b.WriteString(accountEmailStyle.Render(" Set a password to encrypt all data.") + "\n\n")
1257
1258 if m.encFocusIndex == 0 {
1259 b.WriteString(settingsFocusedStyle.Render("Password:\n"))
1260 } else {
1261 b.WriteString(settingsBlurredStyle.Render("Password:\n"))
1262 }
1263 b.WriteString(m.encPasswordInput.View() + "\n\n")
1264
1265 if m.encFocusIndex == 1 {
1266 b.WriteString(settingsFocusedStyle.Render("Confirm Password:\n"))
1267 } else {
1268 b.WriteString(settingsBlurredStyle.Render("Confirm Password:\n"))
1269 }
1270 b.WriteString(m.encConfirmInput.View() + "\n\n")
1271
1272 saveBtn := "[ Enable Encryption ]"
1273 if m.encFocusIndex == 2 {
1274 saveBtn = settingsFocusedStyle.Render(saveBtn)
1275 } else {
1276 saveBtn = settingsBlurredStyle.Render(saveBtn)
1277 }
1278 b.WriteString(saveBtn + "\n")
1279
1280 if m.encEnabling {
1281 b.WriteString("\n" + accountEmailStyle.Render(" Encrypting data...") + "\n")
1282 }
1283 }
1284
1285 if m.encError != "" {
1286 b.WriteString("\n" + dangerStyle.Render(" "+m.encError) + "\n")
1287 }
1288
1289 mainContent := b.String()
1290 helpView := helpStyle.Render("tab/shift+tab: navigate • enter: select • esc: back")
1291
1292 if m.height > 0 {
1293 currentHeight := lipgloss.Height(docStyle.Render(mainContent + helpView))
1294 gap := m.height - currentHeight
1295 if gap > 0 {
1296 mainContent += strings.Repeat("\n", gap)
1297 }
1298 } else {
1299 mainContent += "\n\n"
1300 }
1301
1302 return docStyle.Render(mainContent + helpView)
1303}
1304
1305// UpdateConfig updates the configuration (used when accounts are deleted).
1306func (m *Settings) UpdateConfig(cfg *config.Config) {
1307 m.cfg = cfg
1308 if m.state == SettingsAccounts && m.cursor >= len(cfg.Accounts) {
1309 m.cursor = len(cfg.Accounts)
1310 }
1311}