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