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