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}