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