1package tui
2
3import (
4 "fmt"
5 "strings"
6
7 "github.com/charmbracelet/bubbles/viewport"
8 tea "github.com/charmbracelet/bubbletea"
9 "github.com/charmbracelet/lipgloss"
10 "github.com/floatpane/matcha/fetcher"
11 "github.com/floatpane/matcha/view"
12)
13
14var (
15 emailHeaderStyle = lipgloss.NewStyle().BorderStyle(lipgloss.NormalBorder()).BorderBottom(true).Padding(0, 1)
16 attachmentBoxStyle = lipgloss.NewStyle().Border(lipgloss.NormalBorder(), false, false, false, true).PaddingLeft(2).MarginTop(1)
17)
18
19type EmailView struct {
20 viewport viewport.Model
21 email fetcher.Email
22 emailIndex int
23 attachmentCursor int
24 focusOnAttachments bool
25 accountID string
26 mailbox MailboxKind
27}
28
29func NewEmailView(email fetcher.Email, emailIndex, width, height int, mailbox MailboxKind) *EmailView {
30 // Pass the styles from the tui package to the view package
31 body, err := view.ProcessBody(email.Body, H1Style, H2Style, BodyStyle)
32 if err != nil {
33 body = fmt.Sprintf("Error rendering body: %v", err)
34 }
35
36 // Create header and compute heights that reduce viewport space.
37 header := fmt.Sprintf("From: %s\nSubject: %s", email.From, email.Subject)
38 headerHeight := lipgloss.Height(header) + 2
39
40 attachmentHeight := 0
41 if len(email.Attachments) > 0 {
42 attachmentHeight = len(email.Attachments) + 2
43 }
44
45 // Build viewport with initial size and set wrapped content to fit the width.
46 vp := viewport.New(width, height-headerHeight-attachmentHeight)
47
48 // Wrap the body to the viewport width so lines don't run off-screen.
49 wrapped := wrapBodyToWidth(body, vp.Width)
50 vp.SetContent(wrapped)
51
52 return &EmailView{
53 viewport: vp,
54 email: email,
55 emailIndex: emailIndex,
56 accountID: email.AccountID,
57 mailbox: mailbox,
58 }
59}
60
61func (m *EmailView) Init() tea.Cmd {
62 return nil
63}
64
65func (m *EmailView) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
66 var cmd tea.Cmd
67 var cmds []tea.Cmd
68
69 switch msg := msg.(type) {
70 case tea.KeyMsg:
71 // Handle 'esc' key locally
72 if msg.Type == tea.KeyEsc {
73 if m.focusOnAttachments {
74 m.focusOnAttachments = false
75 return m, nil
76 }
77 return m, func() tea.Msg { return BackToMailboxMsg{Mailbox: m.mailbox} }
78 }
79
80 if m.focusOnAttachments {
81 switch msg.String() {
82 case "up", "k":
83 if m.attachmentCursor > 0 {
84 m.attachmentCursor--
85 }
86 case "down", "j":
87 if m.attachmentCursor < len(m.email.Attachments)-1 {
88 m.attachmentCursor++
89 }
90 case "enter":
91 if len(m.email.Attachments) > 0 {
92 selected := m.email.Attachments[m.attachmentCursor]
93 idx := m.emailIndex
94 accountID := m.accountID
95 return m, func() tea.Msg {
96 return DownloadAttachmentMsg{
97 Index: idx,
98 Filename: selected.Filename,
99 PartID: selected.PartID,
100 Data: selected.Data,
101 AccountID: accountID,
102 Mailbox: m.mailbox,
103 }
104 }
105 }
106 case "tab":
107 m.focusOnAttachments = false
108 }
109 } else {
110 switch msg.String() {
111 case "r":
112 return m, func() tea.Msg { return ReplyToEmailMsg{Email: m.email} }
113 case "d":
114 accountID := m.accountID
115 uid := m.email.UID
116 return m, func() tea.Msg {
117 return DeleteEmailMsg{UID: uid, AccountID: accountID, Mailbox: m.mailbox}
118 }
119 case "a":
120 accountID := m.accountID
121 uid := m.email.UID
122 return m, func() tea.Msg {
123 return ArchiveEmailMsg{UID: uid, AccountID: accountID, Mailbox: m.mailbox}
124 }
125 case "tab":
126 if len(m.email.Attachments) > 0 {
127 m.focusOnAttachments = true
128 }
129 }
130 }
131 case tea.WindowSizeMsg:
132 header := fmt.Sprintf("From: %s\nSubject: %s", m.email.From, m.email.Subject)
133 headerHeight := lipgloss.Height(header) + 2
134 attachmentHeight := 0
135 if len(m.email.Attachments) > 0 {
136 attachmentHeight = len(m.email.Attachments) + 2
137 }
138 // Update viewport dimensions
139 m.viewport.Width = msg.Width
140 m.viewport.Height = msg.Height - headerHeight - attachmentHeight
141
142 // When the window size changes, rewrap the body to the new width
143 body, err := view.ProcessBody(m.email.Body, H1Style, H2Style, BodyStyle)
144 if err != nil {
145 body = fmt.Sprintf("Error rendering body: %v", err)
146 }
147 wrapped := wrapBodyToWidth(body, m.viewport.Width)
148 m.viewport.SetContent(wrapped)
149 }
150
151 m.viewport, cmd = m.viewport.Update(msg)
152 cmds = append(cmds, cmd)
153
154 return m, tea.Batch(cmds...)
155}
156
157func (m *EmailView) View() string {
158 header := fmt.Sprintf("From: %s | Subject: %s", m.email.From, m.email.Subject)
159 styledHeader := emailHeaderStyle.Width(m.viewport.Width).Render(header)
160
161 var help string
162 if m.focusOnAttachments {
163 help = helpStyle.Render("↑/↓: navigate • enter: download • esc/tab: back to email body")
164 } else {
165 help = helpStyle.Render("r: reply • d: delete • a: archive • tab: focus attachments • esc: back to inbox")
166 }
167
168 var attachmentView string
169 if len(m.email.Attachments) > 0 {
170 var b strings.Builder
171 b.WriteString("Attachments:\n")
172 for i, attachment := range m.email.Attachments {
173 cursor := " "
174 style := itemStyle
175 if m.focusOnAttachments && i == m.attachmentCursor {
176 cursor = "> "
177 style = selectedItemStyle
178 }
179 b.WriteString(style.Render(fmt.Sprintf("%s%s", cursor, attachment.Filename)))
180 b.WriteString("\n")
181 }
182 attachmentView = attachmentBoxStyle.Render(b.String())
183 }
184
185 return fmt.Sprintf("%s\n%s\n%s\n%s", styledHeader, m.viewport.View(), attachmentView, help)
186}
187
188// GetAccountID returns the account ID for this email
189func (m *EmailView) GetAccountID() string {
190 return m.accountID
191}
192
193func wrapBodyToWidth(body string, width int) string {
194 return BodyStyle.Width(width).Render(body)
195}
196
197// GetEmail returns the email being viewed
198func (m *EmailView) GetEmail() fetcher.Email {
199 return m.email
200}