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