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