login.go

  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}