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