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)
33
34// Settings displays the settings screen.
35type Settings struct {
36 cfg *config.Config
37 state SettingsState
38 cursor int
39 confirmingDelete bool
40 width int
41 height int
42
43 // S/MIME Config fields
44 editingAccountIdx int
45 focusIndex int
46 smimeCertInput textinput.Model
47 smimeKeyInput textinput.Model
48}
49
50// NewSettings creates a new settings model.
51func NewSettings(cfg *config.Config) *Settings {
52 if cfg == nil {
53 cfg = &config.Config{}
54 }
55
56 tiStyles := ThemedTextInputStyles()
57
58 certInput := textinput.New()
59 certInput.Placeholder = "/path/to/cert.pem"
60 certInput.Prompt = "> "
61 certInput.CharLimit = 256
62 certInput.SetStyles(tiStyles)
63
64 keyInput := textinput.New()
65 keyInput.Placeholder = "/path/to/private_key.pem"
66 keyInput.Prompt = "> "
67 keyInput.CharLimit = 256
68 keyInput.SetStyles(tiStyles)
69
70 return &Settings{
71 cfg: cfg,
72 state: SettingsMain,
73 cursor: 0,
74 smimeCertInput: certInput,
75 smimeKeyInput: keyInput,
76 }
77}
78
79// Init initializes the settings model.
80func (m *Settings) Init() tea.Cmd {
81 return textinput.Blink
82}
83
84// Update handles messages for the settings model.
85func (m *Settings) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
86 var cmds []tea.Cmd
87 var cmd tea.Cmd
88
89 switch msg := msg.(type) {
90 case tea.WindowSizeMsg:
91 m.width = msg.Width
92 m.height = msg.Height
93 m.smimeCertInput.SetWidth(m.width - 6)
94 m.smimeKeyInput.SetWidth(m.width - 6)
95 return m, nil
96
97 case tea.KeyPressMsg:
98 if m.state == SettingsMain {
99 return m.updateMain(msg)
100 } else if m.state == SettingsTheme {
101 return m.updateTheme(msg)
102 } else if m.state == SettingsMailingLists {
103 return m.updateMailingLists(msg)
104 } else if m.state == SettingsSMIMEConfig {
105 var m2 *Settings
106 m2, cmd = m.updateSMIMEConfig(msg)
107 cmds = append(cmds, cmd)
108 return m2, tea.Batch(cmds...)
109 } else {
110 return m.updateAccounts(msg)
111 }
112 }
113
114 if m.state == SettingsSMIMEConfig {
115 m.smimeCertInput, cmd = m.smimeCertInput.Update(msg)
116 cmds = append(cmds, cmd)
117 m.smimeKeyInput, cmd = m.smimeKeyInput.Update(msg)
118 cmds = append(cmds, cmd)
119 }
120
121 return m, tea.Batch(cmds...)
122}
123
124func (m *Settings) updateMain(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
125 switch msg.String() {
126 case "up", "k":
127 if m.cursor > 0 {
128 m.cursor--
129 }
130 case "down", "j":
131 // Options: 0: Email Accounts, 1: Theme, 2: Image Display, 3: Edit Signature, 4: Contextual Tips, 5: Desktop Notifications, 6: Mailing Lists
132 if m.cursor < 6 {
133 m.cursor++
134 }
135 case "enter":
136 switch m.cursor {
137 case 0: // Email Accounts
138 m.state = SettingsAccounts
139 m.cursor = 0
140 return m, nil
141 case 1: // Theme
142 m.state = SettingsTheme
143 // Position cursor on the currently active theme
144 themes := theme.AllThemes()
145 m.cursor = 0
146 for i, t := range themes {
147 if t.Name == theme.ActiveTheme.Name {
148 m.cursor = i
149 break
150 }
151 }
152 return m, nil
153 case 2: // Image Display
154 m.cfg.DisableImages = !m.cfg.DisableImages
155 _ = config.SaveConfig(m.cfg)
156 return m, nil
157 case 3: // Edit Signature
158 return m, func() tea.Msg { return GoToSignatureEditorMsg{} }
159 case 4: // Contextual Tips
160 m.cfg.HideTips = !m.cfg.HideTips
161 _ = config.SaveConfig(m.cfg)
162 return m, nil
163 case 5: // Desktop Notifications
164 m.cfg.DisableNotifications = !m.cfg.DisableNotifications
165 _ = config.SaveConfig(m.cfg)
166 return m, nil
167 case 6: // Mailing Lists
168 m.state = SettingsMailingLists
169 m.cursor = 0
170 return m, nil
171 }
172 case "esc":
173 return m, func() tea.Msg { return GoToChoiceMenuMsg{} }
174 }
175 return m, nil
176}
177
178func (m *Settings) updateAccounts(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
179 if m.confirmingDelete {
180 switch msg.String() {
181 case "y", "Y":
182 if m.cursor < len(m.cfg.Accounts) {
183 accountID := m.cfg.Accounts[m.cursor].ID
184 m.confirmingDelete = false
185 return m, func() tea.Msg {
186 return DeleteAccountMsg{AccountID: accountID}
187 }
188 }
189 case "n", "N", "esc":
190 m.confirmingDelete = false
191 return m, nil
192 }
193 return m, nil
194 }
195
196 switch msg.String() {
197 case "up", "k":
198 if m.cursor > 0 {
199 m.cursor--
200 }
201 case "down", "j":
202 // +1 for "Add Account" option
203 if m.cursor < len(m.cfg.Accounts) {
204 m.cursor++
205 }
206 case "d":
207 // Delete selected account (not the "Add Account" option)
208 if m.cursor < len(m.cfg.Accounts) && len(m.cfg.Accounts) > 0 {
209 m.confirmingDelete = true
210 }
211 case "e":
212 // Edit selected account
213 if m.cursor < len(m.cfg.Accounts) {
214 acc := m.cfg.Accounts[m.cursor]
215 return m, func() tea.Msg {
216 return GoToEditAccountMsg{
217 AccountID: acc.ID,
218 Provider: acc.ServiceProvider,
219 Name: acc.Name,
220 Email: acc.Email,
221 FetchEmail: acc.FetchEmail,
222 IMAPServer: acc.IMAPServer,
223 IMAPPort: acc.IMAPPort,
224 SMTPServer: acc.SMTPServer,
225 SMTPPort: acc.SMTPPort,
226 Protocol: acc.Protocol,
227 JMAPEndpoint: acc.JMAPEndpoint,
228 POP3Server: acc.POP3Server,
229 POP3Port: acc.POP3Port,
230 }
231 }
232 }
233 case "enter":
234 // If cursor is on "Add Account"
235 if m.cursor == len(m.cfg.Accounts) {
236 return m, func() tea.Msg { return GoToAddAccountMsg{} }
237 } else if m.cursor < len(m.cfg.Accounts) {
238 m.editingAccountIdx = m.cursor
239 m.state = SettingsSMIMEConfig
240 m.smimeCertInput.SetValue(m.cfg.Accounts[m.cursor].SMIMECert)
241 m.smimeKeyInput.SetValue(m.cfg.Accounts[m.cursor].SMIMEKey)
242 m.focusIndex = 0
243 m.smimeCertInput.Focus()
244 m.smimeKeyInput.Blur()
245 return m, textinput.Blink
246 }
247 case "esc":
248 m.state = SettingsMain
249 m.cursor = 0
250 return m, nil
251 }
252 return m, nil
253}
254
255func (m *Settings) updateSMIMEConfig(msg tea.KeyPressMsg) (*Settings, tea.Cmd) {
256 var cmds []tea.Cmd
257 var cmd tea.Cmd
258
259 switch msg.String() {
260 case "esc":
261 m.state = SettingsAccounts
262 return m, nil
263 case "tab", "shift+tab", "up", "down":
264 if msg.String() == "shift+tab" || msg.String() == "up" {
265 m.focusIndex--
266 if m.focusIndex < 0 {
267 m.focusIndex = 4
268 }
269 } else {
270 m.focusIndex++
271 if m.focusIndex > 4 {
272 m.focusIndex = 0
273 }
274 }
275
276 m.smimeCertInput.Blur()
277 m.smimeKeyInput.Blur()
278
279 if m.focusIndex == 0 {
280 cmds = append(cmds, m.smimeCertInput.Focus())
281 } else if m.focusIndex == 1 {
282 cmds = append(cmds, m.smimeKeyInput.Focus())
283 }
284 return m, tea.Batch(cmds...)
285 case "enter", " ":
286 if m.focusIndex == 0 && msg.String() == "enter" {
287 m.focusIndex = 1
288 m.smimeCertInput.Blur()
289 cmds = append(cmds, m.smimeKeyInput.Focus())
290 return m, tea.Batch(cmds...)
291 } else if m.focusIndex == 1 && msg.String() == "enter" {
292 m.focusIndex = 2
293 m.smimeKeyInput.Blur()
294 return m, nil
295 } else if m.focusIndex == 2 {
296 if msg.String() == "enter" || msg.String() == " " {
297 m.cfg.Accounts[m.editingAccountIdx].SMIMESignByDefault = !m.cfg.Accounts[m.editingAccountIdx].SMIMESignByDefault
298 }
299 return m, nil
300 } else if m.focusIndex == 3 && msg.String() == "enter" {
301 m.cfg.Accounts[m.editingAccountIdx].SMIMECert = m.smimeCertInput.Value()
302 m.cfg.Accounts[m.editingAccountIdx].SMIMEKey = m.smimeKeyInput.Value()
303 _ = config.SaveConfig(m.cfg)
304 m.state = SettingsAccounts
305 return m, nil
306 } else if m.focusIndex == 4 && msg.String() == "enter" {
307 m.state = SettingsAccounts
308 return m, nil
309 }
310 }
311
312 if m.focusIndex == 0 {
313 m.smimeCertInput, cmd = m.smimeCertInput.Update(msg)
314 cmds = append(cmds, cmd)
315 } else if m.focusIndex == 1 {
316 m.smimeKeyInput, cmd = m.smimeKeyInput.Update(msg)
317 cmds = append(cmds, cmd)
318 }
319
320 return m, tea.Batch(cmds...)
321}
322
323func (m *Settings) updateMailingLists(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
324 if m.confirmingDelete {
325 switch msg.String() {
326 case "y", "Y":
327 if m.cursor < len(m.cfg.MailingLists) {
328 m.cfg.MailingLists = append(m.cfg.MailingLists[:m.cursor], m.cfg.MailingLists[m.cursor+1:]...)
329 _ = config.SaveConfig(m.cfg)
330 if m.cursor >= len(m.cfg.MailingLists) && m.cursor > 0 {
331 m.cursor--
332 }
333 m.confirmingDelete = false
334 }
335 case "n", "N", "esc":
336 m.confirmingDelete = false
337 return m, nil
338 }
339 return m, nil
340 }
341
342 switch msg.String() {
343 case "up", "k":
344 if m.cursor > 0 {
345 m.cursor--
346 }
347 case "down", "j":
348 if m.cursor < len(m.cfg.MailingLists) {
349 m.cursor++
350 }
351 case "d":
352 if m.cursor < len(m.cfg.MailingLists) && len(m.cfg.MailingLists) > 0 {
353 m.confirmingDelete = true
354 }
355 case "e":
356 // Edit selected mailing list
357 if m.cursor < len(m.cfg.MailingLists) {
358 list := m.cfg.MailingLists[m.cursor]
359 idx := m.cursor
360 return m, func() tea.Msg {
361 return GoToEditMailingListMsg{
362 Index: idx,
363 Name: list.Name,
364 Addresses: strings.Join(list.Addresses, ", "),
365 }
366 }
367 }
368 case "enter":
369 if m.cursor == len(m.cfg.MailingLists) {
370 return m, func() tea.Msg { return GoToAddMailingListMsg{} }
371 }
372 case "esc":
373 m.state = SettingsMain
374 m.cursor = 0
375 return m, nil
376 }
377 return m, nil
378}
379
380// View renders the settings screen.
381func (m *Settings) View() tea.View {
382 if m.state == SettingsMain {
383 return tea.NewView(m.viewMain())
384 } else if m.state == SettingsTheme {
385 return tea.NewView(m.viewTheme())
386 } else if m.state == SettingsMailingLists {
387 return tea.NewView(m.viewMailingLists())
388 } else if m.state == SettingsSMIMEConfig {
389 return tea.NewView(m.viewSMIMEConfig())
390 }
391 return tea.NewView(m.viewAccounts())
392}
393
394func (m *Settings) viewMain() string {
395 var b strings.Builder
396
397 b.WriteString(titleStyle.Render("Settings") + "\n\n")
398
399 // Option 0: Email Accounts
400 if m.cursor == 0 {
401 b.WriteString(selectedAccountItemStyle.Render("> Email Accounts"))
402 } else {
403 b.WriteString(accountItemStyle.Render(" Email Accounts"))
404 }
405 b.WriteString("\n")
406
407 // Option 1: Theme
408 themeText := fmt.Sprintf("Theme: %s", theme.ActiveTheme.Name)
409 if m.cursor == 1 {
410 b.WriteString(selectedAccountItemStyle.Render("> " + themeText))
411 } else {
412 b.WriteString(accountItemStyle.Render(" " + themeText))
413 }
414 b.WriteString("\n")
415
416 // Option 2: Image Display
417 status := "ON"
418 if m.cfg.DisableImages {
419 status = "OFF"
420 }
421 text := fmt.Sprintf("Image Display: %s", status)
422 if m.cursor == 2 {
423 b.WriteString(selectedAccountItemStyle.Render("> " + text))
424 } else {
425 b.WriteString(accountItemStyle.Render(" " + text))
426 }
427 b.WriteString("\n")
428
429 // Option 3: Edit Signature
430 sigText := "Edit Signature"
431 if config.HasSignature() {
432 sigText = "Edit Signature (configured)"
433 }
434 if m.cursor == 3 {
435 b.WriteString(selectedAccountItemStyle.Render("> " + sigText))
436 } else {
437 b.WriteString(accountItemStyle.Render(" " + sigText))
438 }
439 b.WriteString("\n")
440
441 // Option 4: Contextual Tips
442 tipsStatus := "ON"
443 if m.cfg.HideTips {
444 tipsStatus = "OFF"
445 }
446 tipsText := fmt.Sprintf("Contextual Tips: %s", tipsStatus)
447 if m.cursor == 4 {
448 b.WriteString(selectedAccountItemStyle.Render("> " + tipsText))
449 } else {
450 b.WriteString(accountItemStyle.Render(" " + tipsText))
451 }
452 b.WriteString("\n")
453
454 // Option 5: Desktop Notifications
455 notifStatus := "ON"
456 if m.cfg.DisableNotifications {
457 notifStatus = "OFF"
458 }
459 notifText := fmt.Sprintf("Desktop Notifications: %s", notifStatus)
460 if m.cursor == 5 {
461 b.WriteString(selectedAccountItemStyle.Render("> " + notifText))
462 } else {
463 b.WriteString(accountItemStyle.Render(" " + notifText))
464 }
465 b.WriteString("\n")
466
467 // Option 6: Mailing Lists
468 mailingListsText := "Mailing Lists"
469 if m.cursor == 6 {
470 b.WriteString(selectedAccountItemStyle.Render("> " + mailingListsText))
471 } else {
472 b.WriteString(accountItemStyle.Render(" " + mailingListsText))
473 }
474 b.WriteString("\n\n")
475
476 if !m.cfg.HideTips {
477 tip := ""
478 switch m.cursor {
479 case 0:
480 tip = "Manage your connected email accounts."
481 case 1:
482 tip = "Choose a color theme for the application."
483 case 2:
484 tip = "Toggle displaying images in emails."
485 case 3:
486 tip = "Configure the signature appended to your outgoing emails."
487 case 4:
488 tip = "Toggle displaying helpful contextual tips like this one."
489 case 5:
490 tip = "Toggle desktop notifications when new mail arrives."
491 case 6:
492 tip = "Manage groups of email addresses to quickly send to multiple people."
493 }
494 if tip != "" {
495 b.WriteString(TipStyle.Render("Tip: "+tip) + "\n\n")
496 }
497 }
498
499 mainContent := b.String()
500 helpView := helpStyle.Render("↑/↓: navigate • enter: select/toggle • esc: back")
501
502 if m.height > 0 {
503 currentHeight := lipgloss.Height(docStyle.Render(mainContent + helpView))
504 gap := m.height - currentHeight
505 if gap > 0 {
506 mainContent += strings.Repeat("\n", gap)
507 }
508 } else {
509 mainContent += "\n\n"
510 }
511
512 return docStyle.Render(mainContent + helpView)
513}
514
515func (m *Settings) viewAccounts() string {
516 var b strings.Builder
517
518 b.WriteString(titleStyle.Render("Account Settings"))
519 b.WriteString("\n\n")
520
521 if len(m.cfg.Accounts) == 0 {
522 b.WriteString(accountEmailStyle.Render(" No accounts configured.\n"))
523 b.WriteString("\n")
524 }
525
526 for i, account := range m.cfg.Accounts {
527 displayName := account.Email
528 if account.Name != "" {
529 displayName = fmt.Sprintf("%s (%s)", account.Name, account.FetchEmail)
530 }
531
532 providerInfo := account.ServiceProvider
533 if account.ServiceProvider == "custom" {
534 providerInfo = fmt.Sprintf("custom: %s", account.IMAPServer)
535 }
536
537 if account.SMIMECert != "" && account.SMIMEKey != "" {
538 providerInfo += " [S/MIME Configured]"
539 }
540
541 line := fmt.Sprintf("%s - %s", displayName, accountEmailStyle.Render(providerInfo))
542
543 if m.cursor == i {
544 b.WriteString(selectedAccountItemStyle.Render(fmt.Sprintf("> %s", line)))
545 } else {
546 b.WriteString(accountItemStyle.Render(fmt.Sprintf(" %s", line)))
547 }
548 b.WriteString("\n")
549 }
550
551 // Add Account option
552 addAccountText := "Add New Account"
553 if m.cursor == len(m.cfg.Accounts) {
554 b.WriteString(selectedAccountItemStyle.Render(fmt.Sprintf("> %s", addAccountText)))
555 } else {
556 b.WriteString(accountItemStyle.Render(fmt.Sprintf(" %s", addAccountText)))
557 }
558 b.WriteString("\n")
559
560 mainContent := b.String()
561 helpView := helpStyle.Render("↑/↓: navigate • enter: select • e: edit • d: delete • esc: back")
562
563 if m.height > 0 {
564 currentHeight := lipgloss.Height(docStyle.Render(mainContent + helpView))
565 gap := m.height - currentHeight
566 if gap > 0 {
567 mainContent += strings.Repeat("\n", gap)
568 }
569 } else {
570 mainContent += "\n\n"
571 }
572
573 if m.confirmingDelete {
574 accountName := m.cfg.Accounts[m.cursor].Email
575 dialog := DialogBoxStyle.Render(
576 lipgloss.JoinVertical(lipgloss.Center,
577 dangerStyle.Render("Delete account?"),
578 accountEmailStyle.Render(accountName),
579 HelpStyle.Render("\n(y/n)"),
580 ),
581 )
582 return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, dialog)
583 }
584
585 return docStyle.Render(mainContent + helpView)
586}
587
588func (m *Settings) viewSMIMEConfig() string {
589 var b strings.Builder
590
591 account := m.cfg.Accounts[m.editingAccountIdx]
592 b.WriteString(titleStyle.Render(fmt.Sprintf("S/MIME Configuration for %s", account.FetchEmail)))
593 b.WriteString("\n\n")
594
595 if m.focusIndex == 0 {
596 b.WriteString(settingsFocusedStyle.Render("Certificate (PEM) Path:\n"))
597 } else {
598 b.WriteString(settingsBlurredStyle.Render("Certificate (PEM) Path:\n"))
599 }
600 b.WriteString(m.smimeCertInput.View() + "\n\n")
601
602 if m.focusIndex == 1 {
603 b.WriteString(settingsFocusedStyle.Render("Private Key (PEM) Path:\n"))
604 } else {
605 b.WriteString(settingsBlurredStyle.Render("Private Key (PEM) Path:\n"))
606 }
607 b.WriteString(m.smimeKeyInput.View() + "\n\n")
608
609 signStatus := "OFF"
610 if account.SMIMESignByDefault {
611 signStatus = "ON"
612 }
613 if m.focusIndex == 2 {
614 b.WriteString(settingsFocusedStyle.Render(fmt.Sprintf("> Sign By Default: %s\n\n", signStatus)))
615 } else {
616 b.WriteString(settingsBlurredStyle.Render(fmt.Sprintf(" Sign By Default: %s\n\n", signStatus)))
617 }
618
619 saveBtn := "[ Save ]"
620 cancelBtn := "[ Cancel ]"
621
622 if m.focusIndex == 3 {
623 saveBtn = settingsFocusedStyle.Render(saveBtn)
624 } else {
625 saveBtn = settingsBlurredStyle.Render(saveBtn)
626 }
627
628 if m.focusIndex == 4 {
629 cancelBtn = settingsFocusedStyle.Render(cancelBtn)
630 } else {
631 cancelBtn = settingsBlurredStyle.Render(cancelBtn)
632 }
633
634 b.WriteString(saveBtn + " " + cancelBtn + "\n\n")
635
636 mainContent := b.String()
637 helpView := helpStyle.Render("tab/shift+tab: navigate • enter: save/next • esc: back")
638
639 if m.height > 0 {
640 currentHeight := lipgloss.Height(docStyle.Render(mainContent + helpView))
641 gap := m.height - currentHeight
642 if gap > 0 {
643 mainContent += strings.Repeat("\n", gap)
644 }
645 } else {
646 mainContent += "\n\n"
647 }
648
649 return docStyle.Render(mainContent + helpView)
650}
651
652func (m *Settings) viewMailingLists() string {
653 var b strings.Builder
654
655 b.WriteString(titleStyle.Render("Mailing Lists"))
656 b.WriteString("\n\n")
657
658 if len(m.cfg.MailingLists) == 0 {
659 b.WriteString(accountEmailStyle.Render(" No mailing lists configured.\n"))
660 b.WriteString("\n")
661 }
662
663 for i, list := range m.cfg.MailingLists {
664 displayName := list.Name
665
666 addrCount := fmt.Sprintf("%d address", len(list.Addresses))
667 if len(list.Addresses) != 1 {
668 addrCount += "es"
669 }
670
671 line := fmt.Sprintf("%s - %s", displayName, accountEmailStyle.Render(addrCount))
672
673 if m.cursor == i {
674 b.WriteString(selectedAccountItemStyle.Render(fmt.Sprintf("> %s", line)))
675 } else {
676 b.WriteString(accountItemStyle.Render(fmt.Sprintf(" %s", line)))
677 }
678 b.WriteString("\n")
679 }
680
681 // Add Mailing List option
682 addListText := "Add New Mailing List"
683 if m.cursor == len(m.cfg.MailingLists) {
684 b.WriteString(selectedAccountItemStyle.Render(fmt.Sprintf("> %s", addListText)))
685 } else {
686 b.WriteString(accountItemStyle.Render(fmt.Sprintf(" %s", addListText)))
687 }
688 b.WriteString("\n")
689
690 helpView := helpStyle.Render("↑/↓: navigate • enter: select • e: edit • d: delete • esc: back")
691 mainContent := b.String()
692
693 if m.height > 0 {
694 contentHeight := strings.Count(mainContent, "\n") + 1
695 gap := m.height - contentHeight - 2 // -2 for margins
696 if gap > 0 {
697 mainContent += strings.Repeat("\n", gap)
698 }
699 } else {
700 mainContent += "\n\n"
701 }
702
703 if m.confirmingDelete {
704 listName := m.cfg.MailingLists[m.cursor].Name
705 dialog := DialogBoxStyle.Render(
706 lipgloss.JoinVertical(lipgloss.Center,
707 dangerStyle.Render("Delete mailing list?"),
708 accountEmailStyle.Render(listName),
709 HelpStyle.Render("\n(y/n)"),
710 ),
711 )
712 return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, dialog)
713 }
714
715 return docStyle.Render(mainContent + helpView)
716}
717
718func (m *Settings) updateTheme(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
719 themes := theme.AllThemes()
720
721 switch msg.String() {
722 case "up", "k":
723 if m.cursor > 0 {
724 m.cursor--
725 }
726 case "down", "j":
727 if m.cursor < len(themes)-1 {
728 m.cursor++
729 }
730 case "enter":
731 if m.cursor < len(themes) {
732 selected := themes[m.cursor]
733 theme.SetTheme(selected.Name)
734 RebuildStyles()
735 m.cfg.Theme = selected.Name
736 _ = config.SaveConfig(m.cfg)
737 }
738 m.state = SettingsMain
739 m.cursor = 1 // Return to Theme option
740 return m, nil
741 case "esc":
742 m.state = SettingsMain
743 m.cursor = 1 // Return to Theme option
744 return m, nil
745 }
746 return m, nil
747}
748
749func (m *Settings) viewTheme() string {
750 themes := theme.AllThemes()
751
752 // Build left panel: theme list
753 var left strings.Builder
754 left.WriteString(titleStyle.Render("Theme") + "\n\n")
755
756 for i, t := range themes {
757 isActive := t.Name == theme.ActiveTheme.Name
758
759 label := t.Name
760 if isActive {
761 label += " (active)"
762 }
763
764 if m.cursor == i {
765 left.WriteString(selectedAccountItemStyle.Render(fmt.Sprintf("> %s", label)))
766 } else {
767 left.WriteString(accountItemStyle.Render(fmt.Sprintf(" %s", label)))
768 }
769 left.WriteString("\n")
770 }
771
772 left.WriteString("\n")
773 if !m.cfg.HideTips {
774 left.WriteString(TipStyle.Render("Tip: Custom themes can be added as\nJSON files in ~/.config/matcha/themes/") + "\n")
775 }
776
777 // Build right panel: theme preview
778 var previewTheme theme.Theme
779 if m.cursor < len(themes) {
780 previewTheme = themes[m.cursor]
781 } else {
782 previewTheme = theme.ActiveTheme
783 }
784 preview := renderThemePreview(previewTheme, m.width)
785
786 // Join panels side by side
787 leftPanel := lipgloss.NewStyle().Width(30).Render(left.String())
788 content := lipgloss.JoinHorizontal(lipgloss.Top, leftPanel, " ", preview)
789
790 helpView := helpStyle.Render("↑/↓: navigate • enter: select • esc: back")
791
792 if m.height > 0 {
793 currentHeight := lipgloss.Height(docStyle.Render(content + "\n" + helpView))
794 gap := m.height - currentHeight
795 if gap > 0 {
796 content += strings.Repeat("\n", gap)
797 }
798 } else {
799 content += "\n\n"
800 }
801
802 return docStyle.Render(content + "\n" + helpView)
803}
804
805// renderThemePreview renders a small mockup showing how a theme looks.
806func renderThemePreview(t theme.Theme, maxWidth int) string {
807 previewWidth := maxWidth - 38 // 30 for left panel + padding/margins
808 if previewWidth < 30 {
809 previewWidth = 30
810 }
811 if previewWidth > 60 {
812 previewWidth = 60
813 }
814
815 accent := lipgloss.NewStyle().Foreground(t.Accent)
816 accentBold := lipgloss.NewStyle().Foreground(t.Accent).Bold(true)
817 secondary := lipgloss.NewStyle().Foreground(t.Secondary)
818 muted := lipgloss.NewStyle().Foreground(t.MutedText)
819 dim := lipgloss.NewStyle().Foreground(t.DimText)
820 danger := lipgloss.NewStyle().Foreground(t.Danger)
821 warn := lipgloss.NewStyle().Foreground(t.Warning)
822 tip := lipgloss.NewStyle().Foreground(t.Tip).Italic(true)
823 link := lipgloss.NewStyle().Foreground(t.Link)
824 title := lipgloss.NewStyle().Foreground(t.AccentText).Background(t.AccentDark).Padding(0, 1)
825 activeTab := lipgloss.NewStyle().Foreground(t.Accent).Bold(true).Underline(true)
826 activeFolder := lipgloss.NewStyle().Background(t.Accent).Foreground(t.Contrast).Bold(true).Padding(0, 1)
827
828 var b strings.Builder
829
830 b.WriteString(title.Render("Preview: "+t.Name) + "\n\n")
831
832 // Fake inbox tabs
833 b.WriteString(activeTab.Render("Inbox") + " " + secondary.Render("Sent") + " " + secondary.Render("Drafts") + "\n")
834 b.WriteString(secondary.Render(strings.Repeat("─", previewWidth)) + "\n")
835
836 // Fake email list
837 b.WriteString(accentBold.Render("> ") + dim.Render("Alice ") + accent.Render("Meeting tomorrow") + " " + muted.Render("2m ago") + "\n")
838 b.WriteString(" " + dim.Render("Bob ") + secondary.Render("Re: Project update") + " " + muted.Render("1h ago") + "\n")
839 b.WriteString(" " + dim.Render("Carol ") + secondary.Render("Quick question") + " " + muted.Render("3h ago") + "\n\n")
840
841 // Folder sidebar sample
842 b.WriteString(accentBold.Render("Folders") + "\n")
843 b.WriteString(activeFolder.Render(" INBOX ") + " " + secondary.Render("Sent") + " " + secondary.Render("Trash") + "\n\n")
844
845 // Status indicators
846 b.WriteString(accentBold.Render("Success: ") + accent.Render("Email sent!") + "\n")
847 b.WriteString(danger.Render("Error: ") + danger.Render("Connection failed") + "\n")
848 b.WriteString(warn.Render("Update available: v2.0") + "\n")
849 b.WriteString(tip.Render("Tip: Press ? for help") + "\n")
850 b.WriteString(link.Render("https://example.com") + "\n")
851
852 box := lipgloss.NewStyle().
853 Border(lipgloss.RoundedBorder()).
854 BorderForeground(t.AccentDark).
855 Padding(1, 2).
856 Width(previewWidth).
857 Render(b.String())
858
859 return box
860}
861
862// UpdateConfig updates the configuration (used when accounts are deleted).
863func (m *Settings) UpdateConfig(cfg *config.Config) {
864 m.cfg = cfg
865 if m.state == SettingsAccounts && m.cursor >= len(cfg.Accounts) {
866 m.cursor = len(cfg.Accounts)
867 }
868}