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