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