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	SettingsMailingLists
 25)
 26
 27// Settings displays the settings screen.
 28type Settings struct {
 29	cfg              *config.Config
 30	state            SettingsState
 31	cursor           int
 32	confirmingDelete bool
 33	width            int
 34	height           int
 35}
 36
 37// NewSettings creates a new settings model.
 38func NewSettings(cfg *config.Config) *Settings {
 39	if cfg == nil {
 40		cfg = &config.Config{}
 41	}
 42	return &Settings{
 43		cfg:    cfg,
 44		state:  SettingsMain,
 45		cursor: 0,
 46	}
 47}
 48
 49// Init initializes the settings model.
 50func (m *Settings) Init() tea.Cmd {
 51	return nil
 52}
 53
 54// Update handles messages for the settings model.
 55func (m *Settings) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 56	switch msg := msg.(type) {
 57	case tea.WindowSizeMsg:
 58		m.width = msg.Width
 59		m.height = msg.Height
 60		return m, nil
 61
 62	case tea.KeyPressMsg:
 63		if m.state == SettingsMain {
 64			return m.updateMain(msg)
 65		} else if m.state == SettingsMailingLists {
 66			return m.updateMailingLists(msg)
 67		} else {
 68			return m.updateAccounts(msg)
 69		}
 70	}
 71	return m, nil
 72}
 73
 74func (m *Settings) updateMain(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
 75	switch msg.String() {
 76	case "up", "k":
 77		if m.cursor > 0 {
 78			m.cursor--
 79		}
 80	case "down", "j":
 81		// Options: 0: Email Accounts, 1: Image Display, 2: Edit Signature, 3: Contextual Tips, 4: Mailing Lists
 82		if m.cursor < 4 {
 83			m.cursor++
 84		}
 85	case "enter":
 86		switch m.cursor {
 87		case 0: // Email Accounts
 88			m.state = SettingsAccounts
 89			m.cursor = 0
 90			return m, nil
 91		case 1: // Image Display
 92			m.cfg.DisableImages = !m.cfg.DisableImages
 93			// Save config immediately
 94			_ = config.SaveConfig(m.cfg)
 95			return m, nil
 96		case 2: // Edit Signature
 97			return m, func() tea.Msg { return GoToSignatureEditorMsg{} }
 98		case 3: // Contextual Tips
 99			m.cfg.HideTips = !m.cfg.HideTips
100			// Save config immediately
101			_ = config.SaveConfig(m.cfg)
102			return m, nil
103		case 4: // Mailing Lists
104			m.state = SettingsMailingLists
105			m.cursor = 0
106			return m, nil
107		}
108	case "esc":
109		return m, func() tea.Msg { return GoToChoiceMenuMsg{} }
110	}
111	return m, nil
112}
113
114func (m *Settings) updateAccounts(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
115	if m.confirmingDelete {
116		switch msg.String() {
117		case "y", "Y":
118			if m.cursor < len(m.cfg.Accounts) {
119				accountID := m.cfg.Accounts[m.cursor].ID
120				m.confirmingDelete = false
121				return m, func() tea.Msg {
122					return DeleteAccountMsg{AccountID: accountID}
123				}
124			}
125		case "n", "N", "esc":
126			m.confirmingDelete = false
127			return m, nil
128		}
129		return m, nil
130	}
131
132	switch msg.String() {
133	case "up", "k":
134		if m.cursor > 0 {
135			m.cursor--
136		}
137	case "down", "j":
138		// +1 for "Add Account" option
139		if m.cursor < len(m.cfg.Accounts) {
140			m.cursor++
141		}
142	case "d":
143		// Delete selected account (not the "Add Account" option)
144		if m.cursor < len(m.cfg.Accounts) && len(m.cfg.Accounts) > 0 {
145			m.confirmingDelete = true
146		}
147	case "enter":
148		// If cursor is on "Add Account"
149		if m.cursor == len(m.cfg.Accounts) {
150			return m, func() tea.Msg { return GoToAddAccountMsg{} }
151		}
152	case "esc":
153		m.state = SettingsMain
154		m.cursor = 0
155		return m, nil
156	}
157	return m, nil
158}
159
160func (m *Settings) updateMailingLists(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
161	if m.confirmingDelete {
162		switch msg.String() {
163		case "y", "Y":
164			if m.cursor < len(m.cfg.MailingLists) {
165				m.cfg.MailingLists = append(m.cfg.MailingLists[:m.cursor], m.cfg.MailingLists[m.cursor+1:]...)
166				_ = config.SaveConfig(m.cfg)
167				if m.cursor >= len(m.cfg.MailingLists) && m.cursor > 0 {
168					m.cursor--
169				}
170				m.confirmingDelete = false
171			}
172		case "n", "N", "esc":
173			m.confirmingDelete = false
174			return m, nil
175		}
176		return m, nil
177	}
178
179	switch msg.String() {
180	case "up", "k":
181		if m.cursor > 0 {
182			m.cursor--
183		}
184	case "down", "j":
185		if m.cursor < len(m.cfg.MailingLists) {
186			m.cursor++
187		}
188	case "d":
189		if m.cursor < len(m.cfg.MailingLists) && len(m.cfg.MailingLists) > 0 {
190			m.confirmingDelete = true
191		}
192	case "enter":
193		if m.cursor == len(m.cfg.MailingLists) {
194			return m, func() tea.Msg { return GoToAddMailingListMsg{} }
195		}
196	case "esc":
197		m.state = SettingsMain
198		m.cursor = 0
199		return m, nil
200	}
201	return m, nil
202}
203
204// View renders the settings screen.
205func (m *Settings) View() tea.View {
206	if m.state == SettingsMain {
207		return tea.NewView(m.viewMain())
208	} else if m.state == SettingsMailingLists {
209		return tea.NewView(m.viewMailingLists())
210	}
211	return tea.NewView(m.viewAccounts())
212}
213
214func (m *Settings) viewMain() string {
215	var b strings.Builder
216
217	b.WriteString(titleStyle.Render("Settings") + "\n\n")
218
219	// Option 0: Email Accounts
220	if m.cursor == 0 {
221		b.WriteString(selectedAccountItemStyle.Render("> Email Accounts"))
222	} else {
223		b.WriteString(accountItemStyle.Render("  Email Accounts"))
224	}
225	b.WriteString("\n")
226
227	// Option 1: Image Display
228	status := "ON"
229	if m.cfg.DisableImages {
230		status = "OFF"
231	}
232	text := fmt.Sprintf("Image Display: %s", status)
233	if m.cursor == 1 {
234		b.WriteString(selectedAccountItemStyle.Render("> " + text))
235	} else {
236		b.WriteString(accountItemStyle.Render("  " + text))
237	}
238	b.WriteString("\n")
239
240	// Option 2: Edit Signature
241	sigText := "Edit Signature"
242	if config.HasSignature() {
243		sigText = "Edit Signature (configured)"
244	}
245	if m.cursor == 2 {
246		b.WriteString(selectedAccountItemStyle.Render("> " + sigText))
247	} else {
248		b.WriteString(accountItemStyle.Render("  " + sigText))
249	}
250	b.WriteString("\n")
251
252	// Option 3: Contextual Tips
253	tipsStatus := "ON"
254	if m.cfg.HideTips {
255		tipsStatus = "OFF"
256	}
257	tipsText := fmt.Sprintf("Contextual Tips: %s", tipsStatus)
258	if m.cursor == 3 {
259		b.WriteString(selectedAccountItemStyle.Render("> " + tipsText))
260	} else {
261		b.WriteString(accountItemStyle.Render("  " + tipsText))
262	}
263	b.WriteString("\n")
264
265	// Option 4: Mailing Lists
266	mailingListsText := "Mailing Lists"
267	if m.cursor == 4 {
268		b.WriteString(selectedAccountItemStyle.Render("> " + mailingListsText))
269	} else {
270		b.WriteString(accountItemStyle.Render("  " + mailingListsText))
271	}
272	b.WriteString("\n\n")
273
274	if !m.cfg.HideTips {
275		tip := ""
276		switch m.cursor {
277		case 0:
278			tip = "Manage your connected email accounts."
279		case 1:
280			tip = "Toggle displaying images in emails."
281		case 2:
282			tip = "Configure the signature appended to your outgoing emails."
283		case 3:
284			tip = "Toggle displaying helpful contextual tips like this one."
285		case 4:
286			tip = "Manage groups of email addresses to quickly send to multiple people."
287		}
288		if tip != "" {
289			b.WriteString(TipStyle.Render("Tip: "+tip) + "\n\n")
290		}
291	}
292
293	mainContent := b.String()
294	helpView := helpStyle.Render("↑/↓: navigate • enter: select/toggle • esc: back")
295
296	if m.height > 0 {
297		currentHeight := lipgloss.Height(docStyle.Render(mainContent + helpView))
298		gap := m.height - currentHeight
299		if gap > 0 {
300			mainContent += strings.Repeat("\n", gap)
301		}
302	} else {
303		mainContent += "\n\n"
304	}
305
306	return docStyle.Render(mainContent + helpView)
307}
308
309func (m *Settings) viewAccounts() string {
310	var b strings.Builder
311
312	b.WriteString(titleStyle.Render("Account Settings"))
313	b.WriteString("\n\n")
314
315	if len(m.cfg.Accounts) == 0 {
316		b.WriteString(accountEmailStyle.Render("  No accounts configured.\n"))
317		b.WriteString("\n")
318	}
319
320	for i, account := range m.cfg.Accounts {
321		displayName := account.Email
322		if account.Name != "" {
323			displayName = fmt.Sprintf("%s (%s)", account.Name, account.FetchEmail)
324		}
325
326		providerInfo := account.ServiceProvider
327		if account.ServiceProvider == "custom" {
328			providerInfo = fmt.Sprintf("custom: %s", account.IMAPServer)
329		}
330
331		line := fmt.Sprintf("%s - %s", displayName, accountEmailStyle.Render(providerInfo))
332
333		if m.cursor == i {
334			b.WriteString(selectedAccountItemStyle.Render(fmt.Sprintf("> %s", line)))
335		} else {
336			b.WriteString(accountItemStyle.Render(fmt.Sprintf("  %s", line)))
337		}
338		b.WriteString("\n")
339	}
340
341	// Add Account option
342	addAccountText := "Add New Account"
343	if m.cursor == len(m.cfg.Accounts) {
344		b.WriteString(selectedAccountItemStyle.Render(fmt.Sprintf("> %s", addAccountText)))
345	} else {
346		b.WriteString(accountItemStyle.Render(fmt.Sprintf("  %s", addAccountText)))
347	}
348	b.WriteString("\n")
349
350	mainContent := b.String()
351	helpView := helpStyle.Render("↑/↓: navigate • enter: select • d: delete account • esc: back")
352
353	if m.height > 0 {
354		currentHeight := lipgloss.Height(docStyle.Render(mainContent + helpView))
355		gap := m.height - currentHeight
356		if gap > 0 {
357			mainContent += strings.Repeat("\n", gap)
358		}
359	} else {
360		mainContent += "\n\n"
361	}
362
363	if m.confirmingDelete {
364		accountName := m.cfg.Accounts[m.cursor].Email
365		dialog := DialogBoxStyle.Render(
366			lipgloss.JoinVertical(lipgloss.Center,
367				dangerStyle.Render("Delete account?"),
368				accountEmailStyle.Render(accountName),
369				HelpStyle.Render("\n(y/n)"),
370			),
371		)
372		return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, dialog)
373	}
374
375	return docStyle.Render(mainContent + helpView)
376}
377
378func (m *Settings) viewMailingLists() string {
379	var b strings.Builder
380
381	b.WriteString(titleStyle.Render("Mailing Lists"))
382	b.WriteString("\n\n")
383
384	if len(m.cfg.MailingLists) == 0 {
385		b.WriteString(accountEmailStyle.Render("  No mailing lists configured.\n"))
386		b.WriteString("\n")
387	}
388
389	for i, list := range m.cfg.MailingLists {
390		displayName := list.Name
391
392		addrCount := fmt.Sprintf("%d address", len(list.Addresses))
393		if len(list.Addresses) != 1 {
394			addrCount += "es"
395		}
396
397		line := fmt.Sprintf("%s - %s", displayName, accountEmailStyle.Render(addrCount))
398
399		if m.cursor == i {
400			b.WriteString(selectedAccountItemStyle.Render(fmt.Sprintf("> %s", line)))
401		} else {
402			b.WriteString(accountItemStyle.Render(fmt.Sprintf("  %s", line)))
403		}
404		b.WriteString("\n")
405	}
406
407	// Add Mailing List option
408	addListText := "Add New Mailing List"
409	if m.cursor == len(m.cfg.MailingLists) {
410		b.WriteString(selectedAccountItemStyle.Render(fmt.Sprintf("> %s", addListText)))
411	} else {
412		b.WriteString(accountItemStyle.Render(fmt.Sprintf("  %s", addListText)))
413	}
414	b.WriteString("\n")
415
416	helpView := helpStyle.Render("↑/↓: navigate • enter: select • d: delete • esc: back")
417	mainContent := b.String()
418
419	if m.height > 0 {
420		contentHeight := strings.Count(mainContent, "\n") + 1
421		gap := m.height - contentHeight - 2 // -2 for margins
422		if gap > 0 {
423			mainContent += strings.Repeat("\n", gap)
424		}
425	} else {
426		mainContent += "\n\n"
427	}
428
429	if m.confirmingDelete {
430		listName := m.cfg.MailingLists[m.cursor].Name
431		dialog := DialogBoxStyle.Render(
432			lipgloss.JoinVertical(lipgloss.Center,
433				dangerStyle.Render("Delete mailing list?"),
434				accountEmailStyle.Render(listName),
435				HelpStyle.Render("\n(y/n)"),
436			),
437		)
438		return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, dialog)
439	}
440
441	return docStyle.Render(mainContent + helpView)
442}
443
444// UpdateConfig updates the configuration (used when accounts are deleted).
445func (m *Settings) UpdateConfig(cfg *config.Config) {
446	m.cfg = cfg
447	if m.state == SettingsAccounts && m.cursor >= len(cfg.Accounts) {
448		m.cursor = len(cfg.Accounts)
449	}
450}