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}