settings.go

  1package tui
  2
  3import (
  4	"fmt"
  5	"strings"
  6
  7	tea "github.com/charmbracelet/bubbletea"
  8	"github.com/charmbracelet/lipgloss"
  9	"github.com/floatpane/matcha/config"
 10)
 11
 12var (
 13	accountItemStyle         = lipgloss.NewStyle().PaddingLeft(2)
 14	selectedAccountItemStyle = lipgloss.NewStyle().PaddingLeft(2).Foreground(lipgloss.Color("42"))
 15	accountEmailStyle        = lipgloss.NewStyle().Foreground(lipgloss.Color("240"))
 16	dangerStyle              = lipgloss.NewStyle().Foreground(lipgloss.Color("196"))
 17)
 18
 19type SettingsState int
 20
 21const (
 22	SettingsMain SettingsState = iota
 23	SettingsAccounts
 24)
 25
 26// Settings displays the settings screen.
 27type Settings struct {
 28	cfg              *config.Config
 29	state            SettingsState
 30	cursor           int
 31	confirmingDelete bool
 32	width            int
 33	height           int
 34}
 35
 36// NewSettings creates a new settings model.
 37func NewSettings(cfg *config.Config) *Settings {
 38	if cfg == nil {
 39		cfg = &config.Config{}
 40	}
 41	return &Settings{
 42		cfg:    cfg,
 43		state:  SettingsMain,
 44		cursor: 0,
 45	}
 46}
 47
 48// Init initializes the settings model.
 49func (m *Settings) Init() tea.Cmd {
 50	return nil
 51}
 52
 53// Update handles messages for the settings model.
 54func (m *Settings) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 55	switch msg := msg.(type) {
 56	case tea.WindowSizeMsg:
 57		m.width = msg.Width
 58		m.height = msg.Height
 59		return m, nil
 60
 61	case tea.KeyMsg:
 62		if m.state == SettingsMain {
 63			return m.updateMain(msg)
 64		} else {
 65			return m.updateAccounts(msg)
 66		}
 67	}
 68	return m, nil
 69}
 70
 71func (m *Settings) updateMain(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
 72	switch msg.String() {
 73	case "up", "k":
 74		if m.cursor > 0 {
 75			m.cursor--
 76		}
 77	case "down", "j":
 78		// Options: 0: Email Accounts, 1: Image Display, 2: Edit Signature
 79		if m.cursor < 2 {
 80			m.cursor++
 81		}
 82	case "enter":
 83		switch m.cursor {
 84		case 0: // Email Accounts
 85			m.state = SettingsAccounts
 86			m.cursor = 0
 87			return m, nil
 88		case 1: // Image Display
 89			m.cfg.DisableImages = !m.cfg.DisableImages
 90			// Save config immediately
 91			_ = config.SaveConfig(m.cfg)
 92			return m, nil
 93		case 2: // Edit Signature
 94			return m, func() tea.Msg { return GoToSignatureEditorMsg{} }
 95		}
 96	case "esc":
 97		return m, func() tea.Msg { return GoToChoiceMenuMsg{} }
 98	}
 99	return m, nil
100}
101
102func (m *Settings) updateAccounts(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
103	if m.confirmingDelete {
104		switch msg.String() {
105		case "y", "Y":
106			if m.cursor < len(m.cfg.Accounts) {
107				accountID := m.cfg.Accounts[m.cursor].ID
108				m.confirmingDelete = false
109				return m, func() tea.Msg {
110					return DeleteAccountMsg{AccountID: accountID}
111				}
112			}
113		case "n", "N", "esc":
114			m.confirmingDelete = false
115			return m, nil
116		}
117		return m, nil
118	}
119
120	switch msg.String() {
121	case "up", "k":
122		if m.cursor > 0 {
123			m.cursor--
124		}
125	case "down", "j":
126		// +1 for "Add Account" option
127		if m.cursor < len(m.cfg.Accounts) {
128			m.cursor++
129		}
130	case "d":
131		// Delete selected account (not the "Add Account" option)
132		if m.cursor < len(m.cfg.Accounts) && len(m.cfg.Accounts) > 0 {
133			m.confirmingDelete = true
134		}
135	case "enter":
136		// If cursor is on "Add Account"
137		if m.cursor == len(m.cfg.Accounts) {
138			return m, func() tea.Msg { return GoToAddAccountMsg{} }
139		}
140	case "esc":
141		m.state = SettingsMain
142		m.cursor = 0
143		return m, nil
144	}
145	return m, nil
146}
147
148// View renders the settings screen.
149func (m *Settings) View() string {
150	if m.state == SettingsMain {
151		return m.viewMain()
152	}
153	return m.viewAccounts()
154}
155
156func (m *Settings) viewMain() string {
157	var b strings.Builder
158
159	b.WriteString(titleStyle.Render("Settings") + "\n\n")
160
161	// Option 0: Email Accounts
162	if m.cursor == 0 {
163		b.WriteString(selectedAccountItemStyle.Render("> Email Accounts"))
164	} else {
165		b.WriteString(accountItemStyle.Render("  Email Accounts"))
166	}
167	b.WriteString("\n")
168
169	// Option 1: Image Display
170	status := "ON"
171	if m.cfg.DisableImages {
172		status = "OFF"
173	}
174	text := fmt.Sprintf("Image Display: %s", status)
175	if m.cursor == 1 {
176		b.WriteString(selectedAccountItemStyle.Render("> " + text))
177	} else {
178		b.WriteString(accountItemStyle.Render("  " + text))
179	}
180	b.WriteString("\n")
181
182	// Option 2: Edit Signature
183	sigText := "Edit Signature"
184	if config.HasSignature() {
185		sigText = "Edit Signature (configured)"
186	}
187	if m.cursor == 2 {
188		b.WriteString(selectedAccountItemStyle.Render("> " + sigText))
189	} else {
190		b.WriteString(accountItemStyle.Render("  " + sigText))
191	}
192	b.WriteString("\n\n")
193
194	b.WriteString(helpStyle.Render("↑/↓: navigate • enter: select/toggle • esc: back"))
195
196	return docStyle.Render(b.String())
197}
198
199func (m *Settings) viewAccounts() string {
200	var b strings.Builder
201
202	b.WriteString(titleStyle.Render("Account Settings"))
203	b.WriteString("\n\n")
204
205	if len(m.cfg.Accounts) == 0 {
206		b.WriteString(accountEmailStyle.Render("  No accounts configured.\n"))
207		b.WriteString("\n")
208	}
209
210	for i, account := range m.cfg.Accounts {
211		displayName := account.Email
212		if account.Name != "" {
213			displayName = fmt.Sprintf("%s (%s)", account.Name, account.FetchEmail)
214		}
215
216		providerInfo := account.ServiceProvider
217		if account.ServiceProvider == "custom" {
218			providerInfo = fmt.Sprintf("custom: %s", account.IMAPServer)
219		}
220
221		line := fmt.Sprintf("%s - %s", displayName, accountEmailStyle.Render(providerInfo))
222
223		if m.cursor == i {
224			b.WriteString(selectedAccountItemStyle.Render(fmt.Sprintf("> %s", line)))
225		} else {
226			b.WriteString(accountItemStyle.Render(fmt.Sprintf("  %s", line)))
227		}
228		b.WriteString("\n")
229	}
230
231	// Add Account option
232	addAccountText := "Add New Account"
233	if m.cursor == len(m.cfg.Accounts) {
234		b.WriteString(selectedAccountItemStyle.Render(fmt.Sprintf("> %s", addAccountText)))
235	} else {
236		b.WriteString(accountItemStyle.Render(fmt.Sprintf("  %s", addAccountText)))
237	}
238	b.WriteString("\n\n")
239
240	b.WriteString(helpStyle.Render("↑/↓: navigate • enter: select • d: delete account • esc: back"))
241
242	if m.confirmingDelete {
243		accountName := m.cfg.Accounts[m.cursor].Email
244		dialog := DialogBoxStyle.Render(
245			lipgloss.JoinVertical(lipgloss.Center,
246				dangerStyle.Render("Delete account?"),
247				accountEmailStyle.Render(accountName),
248				HelpStyle.Render("\n(y/n)"),
249			),
250		)
251		return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, dialog)
252	}
253
254	return docStyle.Render(b.String())
255}
256
257// UpdateConfig updates the configuration (used when accounts are deleted).
258func (m *Settings) UpdateConfig(cfg *config.Config) {
259	m.cfg = cfg
260	if m.state == SettingsAccounts && m.cursor >= len(cfg.Accounts) {
261		m.cursor = len(cfg.Accounts)
262	}
263}