Detailed changes
@@ -2,199 +2,121 @@ package main
import (
"fmt"
+ "log"
"os"
+ "time"
"github.com/andrinoff/email-cli/config"
- "github.com/andrinoff/email-cli/fetcher"
- "github.com/andrinoff/email-cli/view" // Updated package
- "github.com/charmbracelet/bubbles/list"
- "github.com/charmbracelet/bubbles/spinner"
- "github.com/charmbracelet/bubbles/viewport" // Import viewport
+ "github.com/andrinoff/email-cli/sender"
+ "github.com/andrinoff/email-cli/tui"
tea "github.com/charmbracelet/bubbletea"
- "github.com/charmbracelet/lipgloss"
)
-var (
- appStyle = lipgloss.NewStyle().Padding(1, 2)
- headerStyle = lipgloss.NewStyle().
- BorderStyle(lipgloss.NormalBorder()).
- BorderBottom(true)
-)
-
-// emailItem wraps fetcher.Email to satisfy the list.Item interface.
-type emailItem struct {
- fetcher.Email
-}
-
-func (e emailItem) Title() string { return e.Subject }
-func (e emailItem) Description() string { return fmt.Sprintf("From: %s", e.From) }
-func (e emailItem) FilterValue() string { return e.Subject }
-
-type model struct {
- list list.Model
- spinner spinner.Model
- viewport viewport.Model
- state viewState
- selectedItem emailItem
- err error
+// mainModel now holds the state for the entire application.
+type mainModel struct {
+ current tea.Model
+ config *config.Config
+ width int
+ height int
+ err error
}
-type viewState int
-
-const (
- choosingState viewState = iota
- loadingState
- inboxState
- emailViewState
-)
-
-// Messages for tea.Cmd
-type (
- emailsFetchedMsg []fetcher.Email
- errMsg struct{ err error }
-)
-
-func (e errMsg) Error() string { return e.err.Error() }
-
-func newModel() *model {
- s := spinner.New()
- s.Spinner = spinner.Dot
- s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("205"))
-
- initialItems := []list.Item{
- emailItem{fetcher.Email{Subject: "View Inbox"}},
- emailItem{fetcher.Email{Subject: "Send Email (coming soon...)"}},
- }
-
- m := &model{
- spinner: s,
- list: list.New(initialItems, list.NewDefaultDelegate(), 0, 0),
- state: choosingState,
- viewport: viewport.New(10, 10), // Initial size
+// newInitialModel now returns a pointer, which is crucial for state management.
+func newInitialModel(cfg *config.Config) *mainModel {
+ return &mainModel{
+ current: tui.NewChoice(),
+ config: cfg,
}
- m.list.Title = "Email CLI"
- m.list.SetShowStatusBar(false)
- return m
}
-func (m *model) Init() tea.Cmd {
- return nil
+func (m *mainModel) Init() tea.Cmd {
+ return m.current.Init()
}
-func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd
var cmds []tea.Cmd
switch msg := msg.(type) {
case tea.WindowSizeMsg:
- listHeight := msg.Height - 2
- // Adjust viewport size, leaving space for a header
- viewportHeaderHeight := 4
- m.list.SetSize(msg.Width-2, listHeight)
- m.viewport.Width = msg.Width
- m.viewport.Height = msg.Height - viewportHeaderHeight
+ m.width = msg.Width
+ m.height = msg.Height
+ // Pass the window size message to the current view
+ m.current, cmd = m.current.Update(msg)
+ cmds = append(cmds, cmd)
+ return m, tea.Batch(cmds...)
case tea.KeyMsg:
- switch msg.String() {
- case "ctrl+c", "q":
+ if msg.String() == "ctrl+c" {
return m, tea.Quit
- case "esc":
- if m.state == emailViewState {
- m.state = inboxState
- } else if m.state == inboxState {
- // Reset to initial choice list
- initialItems := []list.Item{
- emailItem{fetcher.Email{Subject: "View Inbox"}},
- emailItem{fetcher.Email{Subject: "Send Email (coming soon...)"}},
- }
- m.list.SetItems(initialItems)
- m.list.Title = "Email CLI"
- m.state = choosingState
- }
- return m, nil
- case "enter":
- if m.state == choosingState {
- selected := m.list.SelectedItem().(emailItem)
- if selected.Subject == "View Inbox" {
- m.state = loadingState
- cmds = append(cmds, m.spinner.Tick, fetchEmails)
- }
- } else if m.state == inboxState {
- m.selectedItem = m.list.SelectedItem().(emailItem)
- m.state = emailViewState
-
- // Process body and set viewport content
- body, err := view.ProcessBody(m.selectedItem.Body)
- if err != nil {
- body = fmt.Sprintf("Error rendering body: %v", err)
- }
- m.viewport.SetContent(body)
- m.viewport.GotoTop() // Scroll to top on new email
- }
}
-
- case emailsFetchedMsg:
- items := make([]list.Item, len(msg))
- for i, email := range msg {
- items[i] = emailItem{fetcher.Email(email)}
+ // Allow ESC to go back to the main menu
+ if msg.String() == "esc" {
+ if _, ok := m.current.(*tui.Choice); !ok {
+ m.current = tui.NewChoice()
+ return m, m.current.Init()
+ }
}
- m.list.SetItems(items)
- m.list.Title = "Inbox"
- m.state = inboxState
- case errMsg:
- m.err = msg
- return m, tea.Quit
+ // --- Custom Messages for switching views ---
+ case tui.GoToInboxMsg:
+ m.current = tui.NewInbox()
+ // Manually set the size of the new view
+ m.current, _ = m.current.Update(tea.WindowSizeMsg{Width: m.width, Height: m.height})
+ cmds = append(cmds, m.current.Init())
+
+ case tui.GoToSendMsg:
+ m.current = tui.NewComposer(m.config.Email)
+ m.current, _ = m.current.Update(tea.WindowSizeMsg{Width: m.width, Height: m.height})
+ cmds = append(cmds, m.current.Init())
+
+ case tui.ViewEmailMsg:
+ m.current = tui.NewEmailView(msg.Email, m.width, m.height)
+ cmds = append(cmds, m.current.Init())
+
+ case tui.SendEmailMsg:
+ m.current = tui.NewStatus("Sending email...")
+ cmds = append(cmds, m.current.Init(), sendEmail(m.config, msg))
+
+ case tui.EmailSentMsg:
+ m.current = tui.NewChoice()
+ cmds = append(cmds, m.current.Init())
}
- // Handle updates for the current state
- switch m.state {
- case loadingState:
- m.spinner, cmd = m.spinner.Update(msg)
- case inboxState, choosingState:
- m.list, cmd = m.list.Update(msg)
- case emailViewState:
- m.viewport, cmd = m.viewport.Update(msg)
- }
+ // Pass all other messages to the current view
+ m.current, cmd = m.current.Update(msg)
cmds = append(cmds, cmd)
return m, tea.Batch(cmds...)
}
-func (m *model) View() string {
- if m.err != nil {
- return fmt.Sprintf("Error: %v\n", m.err)
- }
+func (m *mainModel) View() string {
+ return m.current.View()
+}
- switch m.state {
- case loadingState:
- return fmt.Sprintf("\n\n %s Fetching emails...\n\n", m.spinner.View())
- case emailViewState:
- header := fmt.Sprintf("From: %s\nSubject: %s", m.selectedItem.From, m.selectedItem.Subject)
- styledHeader := headerStyle.Width(m.viewport.Width).Render(header)
- return fmt.Sprintf("%s\n%s", styledHeader, m.viewport.View())
- default: // choosingState and inboxState
- return appStyle.Render(m.list.View())
+// sendEmail is a command that sends an email in the background.
+func sendEmail(cfg *config.Config, msg tui.SendEmailMsg) tea.Cmd {
+ return func() tea.Msg {
+ recipients := []string{msg.To}
+ err := sender.SendEmail(cfg, recipients, msg.Subject, msg.Body)
+ if err != nil {
+ log.Printf("Failed to send email: %v", err) // Log error
+ }
+ time.Sleep(1 * time.Second) // Give user time to see the "Sending" message
+ return tui.EmailSentMsg{}
}
}
-func fetchEmails() tea.Msg {
+func main() {
cfg, err := config.LoadConfig()
if err != nil {
- return errMsg{err}
- }
- emails, err := fetcher.FetchEmails(cfg)
- if err != nil {
- return errMsg{err}
+ log.Fatalf("could not load config: %v", err)
}
- return emailsFetchedMsg(emails)
-}
-func main() {
- p := tea.NewProgram(newModel(), tea.WithAltScreen())
+ p := tea.NewProgram(newInitialModel(cfg), tea.WithAltScreen())
if _, err := p.Run(); err != nil {
- fmt.Println("Error running program:", err)
+ fmt.Printf("Alas, there's been an error: %v", err)
os.Exit(1)
}
}
@@ -1,80 +1,57 @@
package tui
import (
- "fmt"
- "os"
-
"github.com/charmbracelet/bubbles/list"
tea "github.com/charmbracelet/bubbletea"
- "github.com/charmbracelet/lipgloss"
)
-type choiceModel struct {
- list list.Model
- choice string
+type Choice struct {
+ list list.Model
+}
+
+func NewChoice() *Choice {
+ items := []list.Item{
+ choiceItem("View Inbox"),
+ choiceItem("Send Email"),
+ }
+
+ l := list.New(items, list.NewDefaultDelegate(), 0, 0)
+ l.Title = "What would you like to do?"
+ l.SetShowStatusBar(false)
+ l.SetFilteringEnabled(false)
+
+ return &Choice{list: l}
}
-func (m choiceModel) Init() tea.Cmd {
+type choiceItem string
+
+func (i choiceItem) FilterValue() string { return "" }
+func (i choiceItem) Title() string { return string(i) }
+func (i choiceItem) Description() string { return "" }
+
+func (m *Choice) Init() tea.Cmd {
return nil
}
-func (m choiceModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+func (m *Choice) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+ var cmd tea.Cmd
switch msg := msg.(type) {
+ case tea.WindowSizeMsg:
+ m.list.SetSize(msg.Width, msg.Height)
case tea.KeyMsg:
- switch keypress := msg.String(); keypress {
- case "ctrl+c", "q":
- return m, tea.Quit
-
- case "enter":
- i, ok := m.list.SelectedItem().(item)
- if ok {
- m.choice = string(i.title)
+ if msg.String() == "enter" {
+ switch m.list.SelectedItem().(choiceItem) {
+ case "View Inbox":
+ return m, func() tea.Msg { return GoToInboxMsg{} }
+ case "Send Email":
+ return m, func() tea.Msg { return GoToSendMsg{} }
}
- return m, tea.Quit
}
- case tea.WindowSizeMsg:
- h, v := DialogBoxStyle.GetFrameSize()
- m.list.SetSize(msg.Width-h, msg.Height-v)
}
-
- var cmd tea.Cmd
m.list, cmd = m.list.Update(msg)
return m, cmd
}
-func (m choiceModel) View() string {
- return DialogBoxStyle.Render(m.list.View())
-}
-
-type item struct {
- title, desc string
-}
-
-func (i item) Title() string { return i.title }
-func (i item) Description() string { return i.desc }
-func (i item) FilterValue() string { return i.title }
-
-func RunChoice() (string, error) {
- items := []list.Item{
- item{title: "send", desc: "Send a new email"},
- item{title: "inbox", desc: "View your inbox"},
- }
-
- l := list.New(items, list.NewDefaultDelegate(), 0, 0)
- l.Title = "What would you like to do?"
- l.Styles.Title = lipgloss.NewStyle().Foreground(lipgloss.Color("205")).Bold(true)
-
- m := choiceModel{list: l}
-
- p := tea.NewProgram(m, tea.WithAltScreen())
-
- mod, err := p.Run()
- if err != nil {
- fmt.Println("Error running program:", err)
- os.Exit(1)
- }
- if m, ok := mod.(choiceModel); ok && m.choice != "" {
- return m.choice, nil
- }
- return "", fmt.Errorf("no choice made")
-}
+func (m *Choice) View() string {
+ return DocStyle.Render(m.list.View())
+}
@@ -1,146 +1,113 @@
package tui
import (
- "fmt"
- "strings"
- "time"
-
- "github.com/andrinoff/email-cli/config"
- "github.com/andrinoff/email-cli/sender"
- "github.com/charmbracelet/bubbles/textarea"
"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
+ "github.com/charmbracelet/lipgloss"
)
-type composerModel struct {
- recipientInput textinput.Model
- subjectInput textinput.Model
- bodyArea textarea.Model
- index int
- sending bool
- sent bool
- err error
- config *config.Config
+type Composer struct {
+ focusIndex int
+ inputs []textinput.Model
+ fromAddr string
}
-func initialComposerModel(cfg *config.Config) composerModel {
- ti := textinput.New()
- ti.Placeholder = "recipient@example.com"
- ti.Focus()
- ti.CharLimit = 156
- ti.Width = 50
- ti.Prompt = "To: "
- si := textinput.New()
- si.Placeholder = "Hello there!"
- si.CharLimit = 156
- si.Width = 50
- si.Prompt = "Subject: "
- ta := textarea.New()
- ta.Placeholder = "Dear friend, ..."
- ta.SetWidth(50)
- ta.SetHeight(10)
- return composerModel{
- recipientInput: ti,
- subjectInput: si,
- bodyArea: ta,
- index: 0,
- config: cfg,
- err: nil,
+func NewComposer(from string) Composer {
+ m := Composer{
+ inputs: make([]textinput.Model, 3),
+ fromAddr: from,
+ }
+
+ var t textinput.Model
+ for i := range m.inputs {
+ t = textinput.New()
+ t.Cursor.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("205"))
+ t.CharLimit = 0 // no limit
+
+ switch i {
+ case 0:
+ t.Placeholder = "To"
+ t.Focus()
+ t.Prompt = "> "
+ case 1:
+ t.Placeholder = "Subject"
+ t.Prompt = "> "
+ case 2:
+ t.Placeholder = "Body..."
+ t.Prompt = "> "
+ }
+ m.inputs[i] = t
}
+ return m
}
-func (m composerModel) Init() tea.Cmd { return textinput.Blink }
+func (m Composer) Init() tea.Cmd {
+ return textinput.Blink
+}
-func (m composerModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
- var cmds []tea.Cmd
+func (m Composer) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
- switch msg.Type {
- case tea.KeyCtrlC, tea.KeyEsc:
- return m, tea.Quit
- case tea.KeyCtrlS:
- if !m.sending {
- m.sending = true
- to := []string{m.recipientInput.Value()}
- subject := m.subjectInput.Value()
- body := m.bodyArea.Value()
- return m, func() tea.Msg {
- err := sender.SendEmail(m.config, to, subject, body)
- if err != nil {
- return err
+ s := msg.String()
+ switch s {
+ case "tab", "shift+tab", "enter", "up", "down":
+ if s == "enter" {
+ // If we're on the last input, send the email
+ if m.focusIndex == len(m.inputs)-1 {
+ return m, func() tea.Msg {
+ return SendEmailMsg{
+ To: m.inputs[0].Value(),
+ Subject: m.inputs[1].Value(),
+ Body: m.inputs[2].Value(),
+ }
}
- return "sent"
}
}
- case tea.KeyTab, tea.KeyShiftTab:
- if msg.Type == tea.KeyTab {
- m.index = (m.index + 1) % 3
+
+ // Cycle focus
+ if s == "up" || s == "shift+tab" {
+ m.focusIndex--
} else {
- m.index--
- if m.index < 0 {
- m.index = 2
- }
+ m.focusIndex++
}
- m.recipientInput.Blur()
- m.subjectInput.Blur()
- m.bodyArea.Blur()
- switch m.index {
- case 0:
- m.recipientInput.Focus()
- case 1:
- m.subjectInput.Focus()
- case 2:
- m.bodyArea.Focus()
+
+ if m.focusIndex > len(m.inputs)-1 {
+ m.focusIndex = 0
+ } else if m.focusIndex < 0 {
+ m.focusIndex = len(m.inputs) - 1
+ }
+
+ cmds := make([]tea.Cmd, len(m.inputs))
+ for i := 0; i < len(m.inputs); i++ {
+ if i == m.focusIndex {
+ cmds[i] = m.inputs[i].Focus()
+ } else {
+ m.inputs[i].Blur()
+ }
}
return m, tea.Batch(cmds...)
}
- case string:
- if msg == "sent" {
- m.sending = false
- m.sent = true
- return m, tea.Sequence(func() tea.Msg { time.Sleep(time.Second); return nil }, tea.Quit)
- }
- case error:
- m.sending = false
- m.err = msg
- return m, nil
}
- var cmd tea.Cmd
- switch m.index {
- case 0:
- m.recipientInput, cmd = m.recipientInput.Update(msg)
- case 1:
- m.subjectInput, cmd = m.subjectInput.Update(msg)
- case 2:
- m.bodyArea, cmd = m.bodyArea.Update(msg)
- }
- cmds = append(cmds, cmd)
- return m, tea.Batch(cmds...)
+
+ // Update the focused input
+ cmd := m.updateInputs(msg)
+ return m, cmd
}
-func (m composerModel) View() string {
- if m.err != nil {
- return InfoStyle.Render(fmt.Sprintf("\n Error: %v \n\n Press any key to exit.", m.err))
- }
- if m.sent {
- return SuccessStyle.Render("\n ✓ Email sent successfully! \n\n")
- }
- if m.sending {
- return InfoStyle.Render("\n Sending email... \n\n")
+func (m *Composer) updateInputs(msg tea.Msg) tea.Cmd {
+ var cmds = make([]tea.Cmd, len(m.inputs))
+ for i := range m.inputs {
+ m.inputs[i], cmds[i] = m.inputs[i].Update(msg)
}
- var s strings.Builder
- s.WriteString("\n")
- s.WriteString(m.recipientInput.View() + "\n")
- s.WriteString(m.subjectInput.View() + "\n")
- s.WriteString(m.bodyArea.View())
- help := fmt.Sprintf("\n\n %s | %s | %s \n", "Tab: Next Field", "Esc: Quit", "Ctrl+S: Send")
- s.WriteString(HelpStyle.Render(help))
- return DialogBoxStyle.Render(s.String())
+ return tea.Batch(cmds...)
}
-// RunComposer starts the email composer UI.
-func RunComposer(cfg *config.Config) error {
- p := tea.NewProgram(initialComposerModel(cfg))
- _, err := p.Run()
- return err
+func (m Composer) View() string {
+ return lipgloss.JoinVertical(
+ lipgloss.Left,
+ "Compose Email (Press Enter on Body to Send)",
+ m.inputs[0].View(),
+ m.inputs[1].View(),
+ m.inputs[2].View(),
+ )
}
@@ -0,0 +1,64 @@
+package tui
+
+import (
+ "fmt"
+
+ "github.com/andrinoff/email-cli/fetcher"
+ "github.com/andrinoff/email-cli/view"
+ "github.com/charmbracelet/bubbles/viewport"
+ tea "github.com/charmbracelet/bubbletea"
+ "github.com/charmbracelet/lipgloss"
+)
+
+var (
+ emailHeaderStyle = lipgloss.NewStyle().
+ BorderStyle(lipgloss.NormalBorder()).
+ BorderBottom(true).
+ Padding(0, 1)
+)
+
+type EmailView struct {
+ viewport viewport.Model
+ email fetcher.Email
+}
+
+func NewEmailView(email fetcher.Email, width, height int) *EmailView {
+ body, err := view.ProcessBody(email.Body)
+ if err != nil {
+ body = fmt.Sprintf("Error rendering body: %v", err)
+ }
+
+ header := fmt.Sprintf("From: %s\nSubject: %s", email.From, email.Subject)
+ headerHeight := lipgloss.Height(header) + 2 // Account for padding and border
+
+ vp := viewport.New(width, height-headerHeight)
+ vp.SetContent(body)
+
+ return &EmailView{
+ viewport: vp,
+ email: email,
+ }
+}
+
+func (m *EmailView) Init() tea.Cmd {
+ return nil
+}
+
+func (m *EmailView) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+ var cmd tea.Cmd
+ switch msg := msg.(type) {
+ case tea.WindowSizeMsg:
+ header := fmt.Sprintf("From: %s\nSubject: %s", m.email.From, m.email.Subject)
+ headerHeight := lipgloss.Height(header) + 2
+ m.viewport.Width = msg.Width
+ m.viewport.Height = msg.Height - headerHeight
+ }
+ m.viewport, cmd = m.viewport.Update(msg)
+ return m, cmd
+}
+
+func (m *EmailView) View() string {
+ header := fmt.Sprintf("From: %s\nSubject: %s", m.email.From, m.email.Subject)
+ styledHeader := emailHeaderStyle.Width(m.viewport.Width).Render(header)
+ return fmt.Sprintf("%s\n%s", styledHeader, m.viewport.View())
+}
@@ -1,10 +1,7 @@
-
-
package tui
import (
"fmt"
- "strings"
"github.com/andrinoff/email-cli/config"
"github.com/andrinoff/email-cli/fetcher"
@@ -14,117 +11,106 @@ import (
"github.com/charmbracelet/lipgloss"
)
-type inboxModel struct {
- spinner spinner.Model
- loading bool
- emails []fetcher.Email
- list list.Model
- err error
- config *config.Config
- selected bool
+// inboxItem wraps fetcher.Email to satisfy the list.Item interface.
+type inboxItem struct {
+ fetcher.Email
}
-func (m inboxModel) Init() tea.Cmd {
- return m.spinner.Tick
+func (i inboxItem) Title() string { return i.Subject }
+func (i inboxItem) Description() string { return fmt.Sprintf("From: %s", i.From) }
+func (i inboxItem) FilterValue() string { return i.Subject }
+
+type Inbox struct {
+ list list.Model
+ spinner spinner.Model
+ loading bool
+ err error
}
-func (m inboxModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+func NewInbox() *Inbox {
+ s := spinner.New()
+ s.Spinner = spinner.Dot
+ s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("205"))
+
+ // Create an empty list for now. It will be populated later.
+ l := list.New([]list.Item{}, list.NewDefaultDelegate(), 0, 0)
+ l.Title = "Inbox"
+
+ return &Inbox{
+ spinner: s,
+ list: l,
+ loading: true,
+ }
+}
+
+func (m *Inbox) Init() tea.Cmd {
+ return tea.Batch(m.spinner.Tick, fetchEmails)
+}
+
+func (m *Inbox) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+ var cmd tea.Cmd
switch msg := msg.(type) {
- case tea.KeyMsg:
- if msg.String() == "ctrl+c" || msg.String() == "q" {
- return m, tea.Quit
- }
- if msg.String() == "enter" {
- m.selected = true
- return m, nil
+ case spinner.TickMsg:
+ if m.loading {
+ m.spinner, cmd = m.spinner.Update(msg)
}
- case tea.WindowSizeMsg:
- h, v := DialogBoxStyle.GetFrameSize()
- m.list.SetSize(msg.Width-h, msg.Height-v)
- case []fetcher.Email:
+ return m, cmd
+
+ case emailsFetchedMsg:
m.loading = false
- m.emails = msg
- items := make([]list.Item, len(m.emails))
- for i, email := range m.emails {
- items[i] = item{
- title: email.Subject,
- desc: fmt.Sprintf("From: %s", email.From),
- }
+ items := make([]list.Item, len(msg))
+ for i, email := range msg {
+ items[i] = inboxItem{email}
}
m.list.SetItems(items)
-
- case error:
- m.err = msg
return m, nil
+
+ case errMsg:
+ m.err = msg.err
+ return m, tea.Quit
+
+ case tea.KeyMsg:
+ if msg.String() == "enter" && !m.loading {
+ selected := m.list.SelectedItem().(inboxItem)
+ return m, func() tea.Msg {
+ return ViewEmailMsg{Email: selected.Email}
+ }
+ }
+
+ case tea.WindowSizeMsg:
+ // When the window size changes, update the list's dimensions.
+ m.list.SetSize(msg.Width, msg.Height)
}
- var cmd tea.Cmd
- if m.loading {
- m.spinner, cmd = m.spinner.Update(msg)
- } else {
+ if !m.loading {
m.list, cmd = m.list.Update(msg)
}
return m, cmd
}
-func (m inboxModel) View() string {
+func (m *Inbox) View() string {
if m.err != nil {
- return InfoStyle.Render(fmt.Sprintf("\n Error: %v \n\n Press any key to exit.", m.err))
+ return fmt.Sprintf("Error: %v", m.err)
}
if m.loading {
- return InfoStyle.Render(fmt.Sprintf("\n %s Fetching emails... \n\n", m.spinner.View()))
- }
- if m.selected {
- i, ok := m.list.SelectedItem().(item)
- if !ok {
- return "error"
- }
- var selectedEmail fetcher.Email
- for _, email := range m.emails {
- if email.Subject == i.Title() {
- selectedEmail = email
- break
- }
- }
- var s strings.Builder
- s.WriteString(fmt.Sprintf("From: %s\n", selectedEmail.From))
- s.WriteString(fmt.Sprintf("Subject: %s\n", selectedEmail.Subject))
- s.WriteString(fmt.Sprintf("Date: %s\n\n", selectedEmail.Date.Format("2006-01-02 15:04:05")))
- s.WriteString(selectedEmail.Body)
- s.WriteString(HelpStyle.Render("\n\n(q to quit)"))
- return DialogBoxStyle.Render(s.String())
-
- }
- return DialogBoxStyle.Render(m.list.View())
-}
-
-func initialInboxModel(cfg *config.Config) inboxModel {
- s := spinner.New()
- s.Spinner = spinner.Dot
- s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("205"))
- l := list.New([]list.Item{}, list.NewDefaultDelegate(), 0, 0)
- l.Title = "Inbox"
- l.Styles.Title = lipgloss.NewStyle().Foreground(lipgloss.Color("205")).Bold(true)
- return inboxModel{
- spinner: s,
- loading: true,
- config: cfg,
- list: l,
+ return fmt.Sprintf("\n\n %s Fetching emails...\n\n", m.spinner.View())
}
+ return DocStyle.Render(m.list.View())
}
-func RunInbox(cfg *config.Config) error {
- m := initialInboxModel(cfg)
- p := tea.NewProgram(m, tea.WithAltScreen())
+// --- Bubble Tea Commands and Messages ---
- go func() {
- emails, err := fetcher.FetchEmails(cfg)
- if err != nil {
- p.Send(err)
- }
- p.Send(emails)
- }()
+type emailsFetchedMsg []fetcher.Email
+type errMsg struct{ err error }
- _, err := p.Run()
- return err
+func fetchEmails() tea.Msg {
+ cfg, err := config.LoadConfig()
+ if err != nil {
+ return errMsg{err}
+ }
+ emails, err := fetcher.FetchEmails(cfg)
+ if err != nil {
+ return errMsg{err}
+ }
+ return emailsFetchedMsg(emails)
}
@@ -0,0 +1,24 @@
+package tui
+
+import "github.com/andrinoff/email-cli/fetcher"
+
+// A message to tell the main model to switch to the inbox view.
+type GoToInboxMsg struct{}
+
+// A message to tell the main model to switch to the composer view.
+type GoToSendMsg struct{}
+
+// A message containing a fetched email to be viewed.
+type ViewEmailMsg struct {
+ Email fetcher.Email
+}
+
+// A message with the content of an email to be sent.
+type SendEmailMsg struct {
+ To string
+ Subject string
+ Body string
+}
+
+// A message to indicate that an email has been successfully sent.
+type EmailSentMsg struct{}
@@ -1,6 +1,12 @@
package tui
-import "github.com/charmbracelet/lipgloss"
+import (
+ "fmt"
+
+ "github.com/charmbracelet/bubbles/spinner"
+ tea "github.com/charmbracelet/bubbletea"
+ "github.com/charmbracelet/lipgloss"
+)
var (
DialogBoxStyle = lipgloss.NewStyle().
@@ -15,4 +21,31 @@ var (
HelpStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("240"))
SuccessStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("42")).Bold(true)
InfoStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("205")).Bold(true)
-)
+)
+
+var DocStyle = lipgloss.NewStyle().Margin(1, 2)
+
+// A simple model for showing a status message
+type Status struct {
+ spinner spinner.Model
+ message string
+}
+
+func NewStatus(msg string) Status {
+ s := spinner.New()
+ s.Spinner = spinner.Dot
+ s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("205"))
+ return Status{spinner: s, message: msg}
+}
+
+func (m Status) Init() tea.Cmd { return m.spinner.Tick }
+
+func (m Status) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+ var cmd tea.Cmd
+ m.spinner, cmd = m.spinner.Update(msg)
+ return m, cmd
+}
+
+func (m Status) View() string {
+ return fmt.Sprintf("\n\n %s %s\n\n", m.spinner.View(), m.message)
+}