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