settings.go

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