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	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}