1package tui
2
3import (
4 "strconv"
5
6 "charm.land/bubbles/v2/textinput"
7 tea "charm.land/bubbletea/v2"
8 "charm.land/lipgloss/v2"
9)
10
11// Login holds the state for the login/add account form.
12type Login struct {
13 focusIndex int
14 inputs []textinput.Model
15 showCustom bool // Show custom server fields
16 isEditMode bool // Whether we're editing an existing account
17 accountID string
18 hideTips bool
19 width int
20 height int
21}
22
23const (
24 inputProvider = iota
25 inputName
26 inputEmail
27 inputFetchEmail
28 inputPassword
29 inputIMAPServer
30 inputIMAPPort
31 inputSMTPServer
32 inputSMTPPort
33 inputCount
34)
35
36// NewLogin creates a new login model for adding accounts.
37func NewLogin(hideTips bool) *Login {
38 m := &Login{
39 inputs: make([]textinput.Model, inputCount),
40 hideTips: hideTips,
41 }
42
43 tiStyles := ThemedTextInputStyles()
44 var t textinput.Model
45 for i := range m.inputs {
46 t = textinput.New()
47 t.CharLimit = 128
48 t.SetStyles(tiStyles)
49
50 switch i {
51 case inputProvider:
52 t.Placeholder = "Provider (gmail, icloud, or custom)"
53 t.Focus()
54 t.Prompt = "🏢 > "
55 case inputName:
56 t.Placeholder = "Display Name"
57 t.Prompt = "👤 > "
58 case inputEmail:
59 t.Placeholder = "Username"
60 t.Prompt = "🏠 > "
61 case inputFetchEmail:
62 t.Placeholder = "Email Address"
63 t.Prompt = "📧 > "
64 case inputPassword:
65 t.Placeholder = "Password / App Password"
66 t.EchoMode = textinput.EchoPassword
67 t.Prompt = "🔑 > "
68 case inputIMAPServer:
69 t.Placeholder = "IMAP Server (e.g., imap.example.com)"
70 t.Prompt = "📥 > "
71 case inputIMAPPort:
72 t.Placeholder = "IMAP Port (default: 993)"
73 t.Prompt = "🔢 > "
74 case inputSMTPServer:
75 t.Placeholder = "SMTP Server (e.g., smtp.example.com)"
76 t.Prompt = "📤 > "
77 case inputSMTPPort:
78 t.Placeholder = "SMTP Port (default: 587)"
79 t.Prompt = "🔢 > "
80 }
81 m.inputs[i] = t
82 }
83
84 return m
85}
86
87// Init initializes the login model.
88func (m *Login) Init() tea.Cmd {
89 return textinput.Blink
90}
91
92// Update handles messages for the login model.
93func (m *Login) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
94 switch msg := msg.(type) {
95 case tea.WindowSizeMsg:
96 m.width = msg.Width
97 m.height = msg.Height
98 for i := range m.inputs {
99 m.inputs[i].SetWidth(msg.Width - 6)
100 }
101
102 case tea.KeyPressMsg:
103 switch msg.String() {
104 case "esc":
105 return m, func() tea.Msg { return GoToChoiceMenuMsg{} }
106
107 case "enter":
108 // Check if provider is "custom" to show/hide custom fields
109 provider := m.inputs[inputProvider].Value()
110 if provider == "custom" {
111 m.showCustom = true
112 } else {
113 m.showCustom = false
114 }
115
116 lastFieldIndex := inputPassword
117 if m.showCustom {
118 lastFieldIndex = inputSMTPPort
119 }
120
121 if m.focusIndex == lastFieldIndex {
122 // Submit the form
123 imapPort := 993
124 smtpPort := 587
125 if m.inputs[inputIMAPPort].Value() != "" {
126 if p, err := strconv.Atoi(m.inputs[inputIMAPPort].Value()); err == nil {
127 imapPort = p
128 }
129 }
130 if m.inputs[inputSMTPPort].Value() != "" {
131 if p, err := strconv.Atoi(m.inputs[inputSMTPPort].Value()); err == nil {
132 smtpPort = p
133 }
134 }
135
136 return m, func() tea.Msg {
137 return Credentials{
138 Provider: m.inputs[inputProvider].Value(),
139 Name: m.inputs[inputName].Value(),
140 Host: m.inputs[inputEmail].Value(),
141 FetchEmail: m.inputs[inputFetchEmail].Value(),
142 Password: m.inputs[inputPassword].Value(),
143 IMAPServer: m.inputs[inputIMAPServer].Value(),
144 IMAPPort: imapPort,
145 SMTPServer: m.inputs[inputSMTPServer].Value(),
146 SMTPPort: smtpPort,
147 }
148 }
149 }
150 fallthrough
151
152 case "tab", "shift+tab", "up", "down":
153 s := msg.String()
154
155 // Check provider to update showCustom
156 provider := m.inputs[inputProvider].Value()
157 m.showCustom = provider == "custom"
158
159 maxIndex := inputPassword
160 if m.showCustom {
161 maxIndex = inputSMTPPort
162 }
163
164 if s == "up" || s == "shift+tab" {
165 m.focusIndex--
166 } else {
167 m.focusIndex++
168 }
169
170 if m.focusIndex > maxIndex {
171 m.focusIndex = 0
172 } else if m.focusIndex < 0 {
173 m.focusIndex = maxIndex
174 }
175
176 // Skip custom fields if not showing them
177 if !m.showCustom && m.focusIndex > inputPassword {
178 if s == "up" || s == "shift+tab" {
179 m.focusIndex = inputPassword
180 } else {
181 m.focusIndex = 0
182 }
183 }
184
185 cmds := make([]tea.Cmd, len(m.inputs))
186 for i := 0; i < len(m.inputs); i++ {
187 if i == m.focusIndex {
188 cmds[i] = m.inputs[i].Focus()
189 } else {
190 m.inputs[i].Blur()
191 }
192 }
193 return m, tea.Batch(cmds...)
194 }
195 }
196
197 // Update the focused input field
198 var cmds = make([]tea.Cmd, len(m.inputs))
199 for i := range m.inputs {
200 m.inputs[i], cmds[i] = m.inputs[i].Update(msg)
201 }
202
203 // Check if provider changed
204 provider := m.inputs[inputProvider].Value()
205 m.showCustom = provider == "custom"
206
207 return m, tea.Batch(cmds...)
208}
209
210// View renders the login form.
211func (m *Login) View() tea.View {
212 title := "Add Account"
213 if m.isEditMode {
214 title = "Edit Account"
215 }
216
217 customHint := ""
218 if m.inputs[inputProvider].Value() == "custom" || m.showCustom {
219 customHint = "\n" + accountEmailStyle.Render("Custom provider selected - configure server settings below")
220 }
221
222 tip := ""
223 switch m.focusIndex {
224 case inputProvider:
225 tip = "Enter your email provider (e.g., gmail, icloud) or 'custom'."
226 case inputName:
227 tip = "The name that will appear on emails you send."
228 case inputEmail:
229 tip = "Your full email address used to log in."
230 case inputFetchEmail:
231 tip = "The email address to fetch messages from (often same as Username)."
232 case inputPassword:
233 tip = "Your password or an app-specific password if using 2FA."
234 case inputIMAPServer:
235 tip = "The server address for receiving emails."
236 case inputIMAPPort:
237 tip = "The port for the IMAP server (usually 993 for SSL)."
238 case inputSMTPServer:
239 tip = "The server address for sending emails."
240 case inputSMTPPort:
241 tip = "The port for the SMTP server (usually 587 for TLS)."
242 }
243
244 views := []string{
245 titleStyle.Render(title),
246 "Enter your email account credentials.",
247 customHint,
248 m.inputs[inputProvider].View(),
249 m.inputs[inputName].View(),
250 m.inputs[inputEmail].View(),
251 m.inputs[inputFetchEmail].View(),
252 m.inputs[inputPassword].View(),
253 }
254
255 if m.showCustom {
256 views = append(views,
257 "",
258 listHeader.Render("Custom Server Settings:"),
259 m.inputs[inputIMAPServer].View(),
260 m.inputs[inputIMAPPort].View(),
261 m.inputs[inputSMTPServer].View(),
262 m.inputs[inputSMTPPort].View(),
263 )
264 }
265
266 views = append(views, "")
267 if !m.hideTips && tip != "" {
268 views = append(views, TipStyle.Render("Tip: "+tip))
269 }
270 views = append(views, helpStyle.Render("\nenter: save • tab: next field • esc: back to menu"))
271
272 return tea.NewView(lipgloss.JoinVertical(lipgloss.Left, views...))
273}
274
275// SetEditMode sets the login form to edit an existing account.
276func (m *Login) SetEditMode(accountID, provider, name, email, fetchEmail, imapServer string, imapPort int, smtpServer string, smtpPort int) {
277 m.isEditMode = true
278 m.accountID = accountID
279 m.inputs[inputProvider].SetValue(provider)
280 m.inputs[inputName].SetValue(name)
281 m.inputs[inputEmail].SetValue(email)
282 m.inputs[inputFetchEmail].SetValue(fetchEmail)
283 m.showCustom = provider == "custom"
284
285 if m.showCustom {
286 m.inputs[inputIMAPServer].SetValue(imapServer)
287 if imapPort != 0 {
288 m.inputs[inputIMAPPort].SetValue(strconv.Itoa(imapPort))
289 }
290 m.inputs[inputSMTPServer].SetValue(smtpServer)
291 if smtpPort != 0 {
292 m.inputs[inputSMTPPort].SetValue(strconv.Itoa(smtpPort))
293 }
294 }
295}
296
297// GetAccountID returns the account ID being edited (if in edit mode).
298func (m *Login) GetAccountID() string {
299 return m.accountID
300}
301
302// IsEditMode returns whether the form is in edit mode.
303func (m *Login) IsEditMode() bool {
304 return m.isEditMode
305}