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