login.go

  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}