main.go

  1package main
  2
  3import (
  4	"fmt"
  5	"os"
  6
  7	"github.com/andrinoff/email-cli/config"
  8	"github.com/andrinoff/email-cli/fetcher"
  9	"github.com/andrinoff/email-cli/view" // Updated package
 10	"github.com/charmbracelet/bubbles/list"
 11	"github.com/charmbracelet/bubbles/spinner"
 12	"github.com/charmbracelet/bubbles/viewport" // Import viewport
 13	tea "github.com/charmbracelet/bubbletea"
 14	"github.com/charmbracelet/lipgloss"
 15)
 16
 17var (
 18	appStyle = lipgloss.NewStyle().Padding(1, 2)
 19	headerStyle = lipgloss.NewStyle().
 20			BorderStyle(lipgloss.NormalBorder()).
 21			BorderBottom(true)
 22)
 23
 24// emailItem wraps fetcher.Email to satisfy the list.Item interface.
 25type emailItem struct {
 26	fetcher.Email
 27}
 28
 29func (e emailItem) Title() string       { return e.Subject }
 30func (e emailItem) Description() string { return fmt.Sprintf("From: %s", e.From) }
 31func (e emailItem) FilterValue() string { return e.Subject }
 32
 33type model struct {
 34	list         list.Model
 35	spinner      spinner.Model
 36	viewport     viewport.Model
 37	state        viewState
 38	selectedItem emailItem
 39	err          error
 40}
 41
 42type viewState int
 43
 44const (
 45	choosingState viewState = iota
 46	loadingState
 47	inboxState
 48	emailViewState
 49)
 50
 51// Messages for tea.Cmd
 52type (
 53	emailsFetchedMsg []fetcher.Email
 54	errMsg           struct{ err error }
 55)
 56
 57func (e errMsg) Error() string { return e.err.Error() }
 58
 59func newModel() *model {
 60	s := spinner.New()
 61	s.Spinner = spinner.Dot
 62	s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("205"))
 63
 64	initialItems := []list.Item{
 65		emailItem{fetcher.Email{Subject: "View Inbox"}},
 66		emailItem{fetcher.Email{Subject: "Send Email (coming soon...)"}},
 67	}
 68
 69	m := &model{
 70		spinner:  s,
 71		list:     list.New(initialItems, list.NewDefaultDelegate(), 0, 0),
 72		state:    choosingState,
 73		viewport: viewport.New(10, 10), // Initial size
 74	}
 75	m.list.Title = "Email CLI"
 76	m.list.SetShowStatusBar(false)
 77	return m
 78}
 79
 80func (m *model) Init() tea.Cmd {
 81	return nil
 82}
 83
 84func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 85	var cmd tea.Cmd
 86	var cmds []tea.Cmd
 87
 88	switch msg := msg.(type) {
 89	case tea.WindowSizeMsg:
 90		listHeight := msg.Height - 2
 91		// Adjust viewport size, leaving space for a header
 92		viewportHeaderHeight := 4
 93		m.list.SetSize(msg.Width-2, listHeight)
 94		m.viewport.Width = msg.Width
 95		m.viewport.Height = msg.Height - viewportHeaderHeight
 96
 97	case tea.KeyMsg:
 98		switch msg.String() {
 99		case "ctrl+c", "q":
100			return m, tea.Quit
101		case "esc":
102			if m.state == emailViewState {
103				m.state = inboxState
104			} else if m.state == inboxState {
105				// Reset to initial choice list
106				initialItems := []list.Item{
107					emailItem{fetcher.Email{Subject: "View Inbox"}},
108					emailItem{fetcher.Email{Subject: "Send Email (coming soon...)"}},
109				}
110				m.list.SetItems(initialItems)
111				m.list.Title = "Email CLI"
112				m.state = choosingState
113			}
114			return m, nil
115		case "enter":
116			if m.state == choosingState {
117				selected := m.list.SelectedItem().(emailItem)
118				if selected.Subject == "View Inbox" {
119					m.state = loadingState
120					cmds = append(cmds, m.spinner.Tick, fetchEmails)
121				}
122			} else if m.state == inboxState {
123				m.selectedItem = m.list.SelectedItem().(emailItem)
124				m.state = emailViewState
125
126				// Process body and set viewport content
127				body, err := view.ProcessBody(m.selectedItem.Body)
128				if err != nil {
129					body = fmt.Sprintf("Error rendering body: %v", err)
130				}
131				m.viewport.SetContent(body)
132				m.viewport.GotoTop() // Scroll to top on new email
133			}
134		}
135
136	case emailsFetchedMsg:
137		items := make([]list.Item, len(msg))
138		for i, email := range msg {
139			items[i] = emailItem{fetcher.Email(email)}
140		}
141		m.list.SetItems(items)
142		m.list.Title = "Inbox"
143		m.state = inboxState
144
145	case errMsg:
146		m.err = msg
147		return m, tea.Quit
148	}
149
150	// Handle updates for the current state
151	switch m.state {
152	case loadingState:
153		m.spinner, cmd = m.spinner.Update(msg)
154	case inboxState, choosingState:
155		m.list, cmd = m.list.Update(msg)
156	case emailViewState:
157		m.viewport, cmd = m.viewport.Update(msg)
158	}
159	cmds = append(cmds, cmd)
160
161	return m, tea.Batch(cmds...)
162}
163
164func (m *model) View() string {
165	if m.err != nil {
166		return fmt.Sprintf("Error: %v\n", m.err)
167	}
168
169	switch m.state {
170	case loadingState:
171		return fmt.Sprintf("\n\n   %s Fetching emails...\n\n", m.spinner.View())
172	case emailViewState:
173		header := fmt.Sprintf("From: %s\nSubject: %s", m.selectedItem.From, m.selectedItem.Subject)
174		styledHeader := headerStyle.Width(m.viewport.Width).Render(header)
175		return fmt.Sprintf("%s\n%s", styledHeader, m.viewport.View())
176	default: // choosingState and inboxState
177		return appStyle.Render(m.list.View())
178	}
179}
180
181func fetchEmails() tea.Msg {
182	cfg, err := config.LoadConfig()
183	if err != nil {
184		return errMsg{err}
185	}
186	emails, err := fetcher.FetchEmails(cfg)
187	if err != nil {
188		return errMsg{err}
189	}
190	return emailsFetchedMsg(emails)
191}
192
193func main() {
194	p := tea.NewProgram(newModel(), tea.WithAltScreen())
195
196	if _, err := p.Run(); err != nil {
197		fmt.Println("Error running program:", err)
198		os.Exit(1)
199	}
200}