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}