settings.go

  1package tui
  2
  3import (
  4	"fmt"
  5	"strings"
  6
  7	tea "charm.land/bubbletea/v2"
  8	"charm.land/lipgloss/v2"
  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.KeyPressMsg:
 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.KeyPressMsg) (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, 3: Contextual Tips
 79		if m.cursor < 3 {
 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		case 3: // Contextual Tips
 96			m.cfg.HideTips = !m.cfg.HideTips
 97			// Save config immediately
 98			_ = config.SaveConfig(m.cfg)
 99			return m, nil
100		}
101	case "esc":
102		return m, func() tea.Msg { return GoToChoiceMenuMsg{} }
103	}
104	return m, nil
105}
106
107func (m *Settings) updateAccounts(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
108	if m.confirmingDelete {
109		switch msg.String() {
110		case "y", "Y":
111			if m.cursor < len(m.cfg.Accounts) {
112				accountID := m.cfg.Accounts[m.cursor].ID
113				m.confirmingDelete = false
114				return m, func() tea.Msg {
115					return DeleteAccountMsg{AccountID: accountID}
116				}
117			}
118		case "n", "N", "esc":
119			m.confirmingDelete = false
120			return m, nil
121		}
122		return m, nil
123	}
124
125	switch msg.String() {
126	case "up", "k":
127		if m.cursor > 0 {
128			m.cursor--
129		}
130	case "down", "j":
131		// +1 for "Add Account" option
132		if m.cursor < len(m.cfg.Accounts) {
133			m.cursor++
134		}
135	case "d":
136		// Delete selected account (not the "Add Account" option)
137		if m.cursor < len(m.cfg.Accounts) && len(m.cfg.Accounts) > 0 {
138			m.confirmingDelete = true
139		}
140	case "enter":
141		// If cursor is on "Add Account"
142		if m.cursor == len(m.cfg.Accounts) {
143			return m, func() tea.Msg { return GoToAddAccountMsg{} }
144		}
145	case "esc":
146		m.state = SettingsMain
147		m.cursor = 0
148		return m, nil
149	}
150	return m, nil
151}
152
153// View renders the settings screen.
154func (m *Settings) View() tea.View {
155	if m.state == SettingsMain {
156		return tea.NewView(m.viewMain())
157	}
158	return tea.NewView(m.viewAccounts())
159}
160
161func (m *Settings) viewMain() string {
162	var b strings.Builder
163
164	b.WriteString(titleStyle.Render("Settings") + "\n\n")
165
166	// Option 0: Email Accounts
167	if m.cursor == 0 {
168		b.WriteString(selectedAccountItemStyle.Render("> Email Accounts"))
169	} else {
170		b.WriteString(accountItemStyle.Render("  Email Accounts"))
171	}
172	b.WriteString("\n")
173
174	// Option 1: Image Display
175	status := "ON"
176	if m.cfg.DisableImages {
177		status = "OFF"
178	}
179	text := fmt.Sprintf("Image Display: %s", status)
180	if m.cursor == 1 {
181		b.WriteString(selectedAccountItemStyle.Render("> " + text))
182	} else {
183		b.WriteString(accountItemStyle.Render("  " + text))
184	}
185	b.WriteString("\n")
186
187	// Option 2: Edit Signature
188	sigText := "Edit Signature"
189	if config.HasSignature() {
190		sigText = "Edit Signature (configured)"
191	}
192	if m.cursor == 2 {
193		b.WriteString(selectedAccountItemStyle.Render("> " + sigText))
194	} else {
195		b.WriteString(accountItemStyle.Render("  " + sigText))
196	}
197	b.WriteString("\n")
198
199	// Option 3: Contextual Tips
200	tipsStatus := "ON"
201	if m.cfg.HideTips {
202		tipsStatus = "OFF"
203	}
204	tipsText := fmt.Sprintf("Contextual Tips: %s", tipsStatus)
205	if m.cursor == 3 {
206		b.WriteString(selectedAccountItemStyle.Render("> " + tipsText))
207	} else {
208		b.WriteString(accountItemStyle.Render("  " + tipsText))
209	}
210	b.WriteString("\n\n")
211
212	if !m.cfg.HideTips {
213		tip := ""
214		switch m.cursor {
215		case 0:
216			tip = "Manage your connected email accounts."
217		case 1:
218			tip = "Toggle displaying images in emails."
219		case 2:
220			tip = "Configure the signature appended to your outgoing emails."
221		case 3:
222			tip = "Toggle displaying helpful contextual tips like this one."
223		}
224		if tip != "" {
225			b.WriteString(TipStyle.Render("Tip: "+tip) + "\n\n")
226		}
227	}
228
229	b.WriteString(helpStyle.Render("↑/↓: navigate • enter: select/toggle • esc: back"))
230
231	return docStyle.Render(b.String())
232}
233
234func (m *Settings) viewAccounts() string {
235	var b strings.Builder
236
237	b.WriteString(titleStyle.Render("Account Settings"))
238	b.WriteString("\n\n")
239
240	if len(m.cfg.Accounts) == 0 {
241		b.WriteString(accountEmailStyle.Render("  No accounts configured.\n"))
242		b.WriteString("\n")
243	}
244
245	for i, account := range m.cfg.Accounts {
246		displayName := account.Email
247		if account.Name != "" {
248			displayName = fmt.Sprintf("%s (%s)", account.Name, account.FetchEmail)
249		}
250
251		providerInfo := account.ServiceProvider
252		if account.ServiceProvider == "custom" {
253			providerInfo = fmt.Sprintf("custom: %s", account.IMAPServer)
254		}
255
256		line := fmt.Sprintf("%s - %s", displayName, accountEmailStyle.Render(providerInfo))
257
258		if m.cursor == i {
259			b.WriteString(selectedAccountItemStyle.Render(fmt.Sprintf("> %s", line)))
260		} else {
261			b.WriteString(accountItemStyle.Render(fmt.Sprintf("  %s", line)))
262		}
263		b.WriteString("\n")
264	}
265
266	// Add Account option
267	addAccountText := "Add New Account"
268	if m.cursor == len(m.cfg.Accounts) {
269		b.WriteString(selectedAccountItemStyle.Render(fmt.Sprintf("> %s", addAccountText)))
270	} else {
271		b.WriteString(accountItemStyle.Render(fmt.Sprintf("  %s", addAccountText)))
272	}
273	b.WriteString("\n\n")
274
275	b.WriteString(helpStyle.Render("↑/↓: navigate • enter: select • d: delete account • esc: back"))
276
277	if m.confirmingDelete {
278		accountName := m.cfg.Accounts[m.cursor].Email
279		dialog := DialogBoxStyle.Render(
280			lipgloss.JoinVertical(lipgloss.Center,
281				dangerStyle.Render("Delete account?"),
282				accountEmailStyle.Render(accountName),
283				HelpStyle.Render("\n(y/n)"),
284			),
285		)
286		return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, dialog)
287	}
288
289	return docStyle.Render(b.String())
290}
291
292// UpdateConfig updates the configuration (used when accounts are deleted).
293func (m *Settings) UpdateConfig(cfg *config.Config) {
294	m.cfg = cfg
295	if m.state == SettingsAccounts && m.cursor >= len(cfg.Accounts) {
296		m.cursor = len(cfg.Accounts)
297	}
298}