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