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