From a33ede7e5f4d078bfc0822082a0ce9d513a8d8f5 Mon Sep 17 00:00:00 2001 From: drew Date: Mon, 28 Jul 2025 12:34:17 +0400 Subject: [PATCH] fix: turned the send_mail back up --- main.go | 226 +++++++++++++++------------------------------- tui/choice.go | 95 ++++++++----------- tui/composer.go | 199 +++++++++++++++++----------------------- tui/email_view.go | 64 +++++++++++++ tui/inbox.go | 168 ++++++++++++++++------------------ tui/messages.go | 24 +++++ tui/styles.go | 37 +++++++- 7 files changed, 393 insertions(+), 420 deletions(-) create mode 100644 tui/email_view.go create mode 100644 tui/messages.go diff --git a/main.go b/main.go index 863bd198a7396ebbfbc6a51c83cc79182e462b09..8243d30ccb569fe447ec6aa1817d2ea376a5df0d 100644 --- a/main.go +++ b/main.go @@ -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) } } \ No newline at end of file diff --git a/tui/choice.go b/tui/choice.go index 0edecf1203a2c241539b606c13e2040d895866b3..2d09abd287f7f0715af57cf3066c26a6a94412aa 100644 --- a/tui/choice.go +++ b/tui/choice.go @@ -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()) +} \ No newline at end of file diff --git a/tui/composer.go b/tui/composer.go index 8ecf1199c5a52e6d169f0f660f6cbde2a06bcf4e..fdad8f82dd8dbc7225a7275443eb235ed81a23be 100644 --- a/tui/composer.go +++ b/tui/composer.go @@ -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(), + ) } \ No newline at end of file diff --git a/tui/email_view.go b/tui/email_view.go new file mode 100644 index 0000000000000000000000000000000000000000..ac1778a7674523b4fe722f1c4a1593f5b898f13c --- /dev/null +++ b/tui/email_view.go @@ -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()) +} \ No newline at end of file diff --git a/tui/inbox.go b/tui/inbox.go index 856272b9b6d1fb71c30a416ad09736c1bb2e82ff..5dc05ff0be662e6f2812142ea7f28641d7fa5137 100644 --- a/tui/inbox.go +++ b/tui/inbox.go @@ -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) } \ No newline at end of file diff --git a/tui/messages.go b/tui/messages.go new file mode 100644 index 0000000000000000000000000000000000000000..071c256552de85791f78e3e3cfcdd8504434000a --- /dev/null +++ b/tui/messages.go @@ -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{} \ No newline at end of file diff --git a/tui/styles.go b/tui/styles.go index 30b89078231c016347fa1c284bf61863611f546a..1fc90a2aeea7e7fe2dce73279d04cbbe9e36db22 100644 --- a/tui/styles.go +++ b/tui/styles.go @@ -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) -) \ No newline at end of file +) + +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) +} \ No newline at end of file