1package tui
2
3import (
4 "fmt"
5 "io"
6 "strings"
7 "time"
8
9 "charm.land/bubbles/v2/key"
10 "charm.land/bubbles/v2/list"
11 tea "charm.land/bubbletea/v2"
12 "charm.land/lipgloss/v2"
13 "github.com/floatpane/matcha/config"
14 "github.com/floatpane/matcha/fetcher"
15 "github.com/floatpane/matcha/theme"
16)
17
18var (
19 // In bubbles v2, list.DefaultStyles() takes a boolean for hasDarkBackground
20 paginationStyle = list.DefaultStyles(true).PaginationStyle.PaddingLeft(4)
21 inboxHelpStyle = list.DefaultStyles(true).HelpStyle.PaddingLeft(4).PaddingBottom(1)
22 tabStyle = lipgloss.NewStyle().Padding(0, 2)
23 activeTabStyle = lipgloss.NewStyle().Padding(0, 2).Foreground(lipgloss.Color("42")).Bold(true).Underline(true)
24 tabBarStyle = lipgloss.NewStyle().BorderStyle(lipgloss.NormalBorder()).BorderBottom(true).PaddingBottom(1).MarginBottom(1)
25)
26
27var dateStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("243"))
28var unreadEmailStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("42")).Bold(true)
29var readEmailStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("240"))
30
31type item struct {
32 title, desc string
33 originalIndex int
34 uid uint32
35 accountID string
36 accountEmail string
37 date time.Time
38 isRead bool
39}
40
41func (i item) Title() string { return i.title }
42func (i item) Description() string { return i.desc }
43func (i item) FilterValue() string { return i.title + " " + i.desc }
44
45type itemDelegate struct{}
46
47func (d itemDelegate) Height() int { return 1 }
48func (d itemDelegate) Spacing() int { return 0 }
49func (d itemDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd { return nil }
50func (d itemDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) {
51 i, ok := listItem.(item)
52 if !ok {
53 return
54 }
55
56 prefix := fmt.Sprintf("%d. ", index+1)
57 sender := parseSenderName(i.desc)
58 statusStyle := unreadEmailStyle
59 statusIcon := "\uf0e0"
60 if i.isRead {
61 statusStyle = readEmailStyle
62 statusIcon = "\uf2b6"
63 }
64 styledIcon := statusStyle.Render(statusIcon)
65 styledSender := statusStyle.Render(sender)
66 separator := " · "
67
68 // For "ALL" view, show account indicator instead of number
69 if i.accountEmail != "" {
70 prefix = fmt.Sprintf("%d. [%s] ", index+1, truncateEmail(i.accountEmail))
71 }
72
73 // Format and right-align date
74 dateStr := formatRelativeDate(i.date)
75 styledDate := dateStyle.Render(dateStr)
76 if i.isRead {
77 styledDate = readEmailStyle.Render(dateStr)
78 } else {
79 styledDate = statusStyle.Render(dateStr)
80 }
81 dateWidth := lipgloss.Width(styledDate)
82
83 listWidth := m.Width()
84 isSelected := index == m.Index()
85 cursorWidth := 0
86 if isSelected {
87 cursorWidth = 2 // "> " prefix
88 }
89
90 // Available width for the whole left side (prefix + sender + separator + subject)
91 maxLeft := listWidth - dateWidth - 2 - cursorWidth // 2 for spacing
92 if maxLeft < 10 {
93 maxLeft = 10
94 }
95
96 prefixWidth := lipgloss.Width(prefix)
97 iconWidth := lipgloss.Width(styledIcon) + 1
98 senderWidth := lipgloss.Width(styledSender)
99 sepWidth := len(separator)
100 subjectBudget := maxLeft - prefixWidth - iconWidth - senderWidth - sepWidth
101
102 subject := i.title
103 if subjectBudget < 4 {
104 subjectBudget = 4
105 }
106 if lipgloss.Width(subject) > subjectBudget {
107 for lipgloss.Width(subject) > subjectBudget-1 && len(subject) > 0 {
108 subject = subject[:len(subject)-1]
109 }
110 subject += "…"
111 }
112 styledSubject := statusStyle.Render(subject)
113
114 str := prefix + styledIcon + " " + styledSender + separator + styledSubject
115
116 // Pad to push date to the right
117 padding := listWidth - lipgloss.Width(str) - dateWidth - cursorWidth
118 if padding < 1 {
119 padding = 1
120 }
121
122 fn := itemStyle.Render
123 if index == m.Index() {
124 fn = func(s ...string) string {
125 return selectedItemStyle.Render("> " + s[0])
126 }
127 }
128
129 fmt.Fprint(w, fn(str+strings.Repeat(" ", padding)+styledDate))
130}
131
132// formatRelativeDate formats a time as relative if within the last week,
133// otherwise as an absolute date.
134func formatRelativeDate(t time.Time) string {
135 if t.IsZero() {
136 return ""
137 }
138 now := time.Now()
139 d := now.Sub(t)
140
141 switch {
142 case d < time.Minute:
143 return "just now"
144 case d < time.Hour:
145 mins := int(d.Minutes())
146 if mins == 1 {
147 return "1 min ago"
148 }
149 return fmt.Sprintf("%d min ago", mins)
150 case d < 24*time.Hour:
151 hours := int(d.Hours())
152 if hours == 1 {
153 return "1 hour ago"
154 }
155 return fmt.Sprintf("%d hours ago", hours)
156 case d < 7*24*time.Hour:
157 days := int(d.Hours() / 24)
158 if days == 1 {
159 return "1 day ago"
160 }
161 return fmt.Sprintf("%d days ago", days)
162 default:
163 if t.Year() == now.Year() {
164 return t.Format("Jan 02")
165 }
166 return t.Format("Jan 02, 2006")
167 }
168}
169
170// parseSenderName extracts the display name from a "Name <email>" string,
171// falling back to the local part of the email address.
172func parseSenderName(from string) string {
173 if idx := strings.Index(from, " <"); idx > 0 {
174 return strings.TrimSpace(from[:idx])
175 }
176 // No display name — use local part of email
177 if idx := strings.Index(from, "@"); idx > 0 {
178 return from[:idx]
179 }
180 return from
181}
182
183// truncateEmail shortens an email for display
184func truncateEmail(email string) string {
185 parts := strings.Split(email, "@")
186 if len(parts) >= 1 && len(parts[0]) > 8 {
187 return parts[0][:8] + "..."
188 }
189 if len(parts) >= 1 {
190 return parts[0]
191 }
192 return email
193}
194
195// AccountTab represents a tab for an account
196type AccountTab struct {
197 ID string
198 Label string
199 Email string
200}
201
202type Inbox struct {
203 list list.Model
204 isFetching bool
205 isRefreshing bool
206 emailsCount int
207 accounts []config.Account
208 emailsByAccount map[string][]fetcher.Email
209 allEmails []fetcher.Email
210 tabs []AccountTab
211 activeTabIndex int
212 width int
213 height int
214 currentAccountID string // Empty means "ALL"
215 emailCountByAcct map[string]int
216 mailbox MailboxKind
217 folderName string // Custom folder name override for title
218 noMoreByAccount map[string]bool // Per-account: true when pagination returns 0 results
219 extraShortHelpKeys []key.Binding
220 pluginStatus string // Persistent status text set by plugins
221}
222
223func NewInbox(emails []fetcher.Email, accounts []config.Account) *Inbox {
224 return NewInboxWithMailbox(emails, accounts, MailboxInbox)
225}
226
227func NewSentInbox(emails []fetcher.Email, accounts []config.Account) *Inbox {
228 return NewInboxWithMailbox(emails, accounts, MailboxSent)
229}
230
231func NewTrashInbox(emails []fetcher.Email, accounts []config.Account) *Inbox {
232 return NewInboxWithMailbox(emails, accounts, MailboxTrash)
233}
234
235func NewArchiveInbox(emails []fetcher.Email, accounts []config.Account) *Inbox {
236 return NewInboxWithMailbox(emails, accounts, MailboxArchive)
237}
238
239func NewInboxWithMailbox(emails []fetcher.Email, accounts []config.Account, mailbox MailboxKind) *Inbox {
240 // Build tabs: empty for single account, "ALL" + accounts for multiple
241 var tabs []AccountTab
242 if len(accounts) <= 1 {
243 tabs = []AccountTab{{ID: "", Label: "", Email: ""}}
244 } else {
245 tabs = []AccountTab{{ID: "", Label: "ALL", Email: ""}}
246 for _, acc := range accounts {
247 // Use FetchEmail for display, fall back to Email if not set
248 displayEmail := acc.FetchEmail
249 if displayEmail == "" {
250 displayEmail = acc.Email
251 }
252 tabs = append(tabs, AccountTab{ID: acc.ID, Label: displayEmail, Email: displayEmail})
253 }
254 }
255
256 // Group emails by account
257 emailsByAccount := make(map[string][]fetcher.Email)
258 for _, email := range emails {
259 emailsByAccount[email.AccountID] = append(emailsByAccount[email.AccountID], email)
260 }
261
262 // Track email counts per account
263 emailCountByAcct := make(map[string]int)
264 for accID, accEmails := range emailsByAccount {
265 emailCountByAcct[accID] = len(accEmails)
266 }
267
268 inbox := &Inbox{
269 accounts: accounts,
270 emailsByAccount: emailsByAccount,
271 allEmails: emails,
272 tabs: tabs,
273 activeTabIndex: 0,
274 currentAccountID: "",
275 emailCountByAcct: emailCountByAcct,
276 mailbox: mailbox,
277 }
278
279 inbox.updateList()
280 return inbox
281}
282
283// NewInboxSingleAccount creates an inbox for a single account (legacy support)
284func NewInboxSingleAccount(emails []fetcher.Email) *Inbox {
285 return NewInbox(emails, nil)
286}
287
288func (m *Inbox) updateList() {
289 // Capture current index to restore later
290 currentIndex := m.list.Index()
291
292 var displayEmails []fetcher.Email
293 var showAccountLabel bool
294
295 if m.currentAccountID == "" {
296 // "ALL" view - show all emails sorted by date
297 displayEmails = m.allEmails
298 showAccountLabel = !(len(m.accounts) <= 1)
299 } else {
300 // Specific account view
301 displayEmails = m.emailsByAccount[m.currentAccountID]
302 showAccountLabel = false
303 }
304
305 m.emailsCount = len(displayEmails)
306
307 items := make([]list.Item, len(displayEmails))
308 for i, email := range displayEmails {
309 accountEmail := ""
310 if showAccountLabel {
311 // Find the account email for display
312 for _, acc := range m.accounts {
313 if acc.ID == email.AccountID {
314 accountEmail = acc.FetchEmail
315 break
316 }
317 }
318 }
319
320 items[i] = item{
321 title: email.Subject,
322 desc: email.From,
323 originalIndex: i,
324 uid: email.UID,
325 accountID: email.AccountID,
326 accountEmail: accountEmail,
327 date: email.Date,
328 isRead: email.IsRead,
329 }
330 }
331
332 l := list.New(items, itemDelegate{}, 20, 14)
333 l.Title = m.getTitle()
334 l.SetShowStatusBar(true)
335 l.SetFilteringEnabled(true)
336 l.Styles.Title = lipgloss.NewStyle().Foreground(theme.ActiveTheme.Accent).Bold(true)
337 l.Styles.PaginationStyle = paginationStyle
338 l.Styles.HelpStyle = inboxHelpStyle
339 l.SetStatusBarItemName("email", "emails")
340 l.AdditionalShortHelpKeys = func() []key.Binding {
341 bindings := []key.Binding{
342 key.NewBinding(key.WithKeys("d"), key.WithHelp("\uf014 d", "delete")),
343 key.NewBinding(key.WithKeys("a"), key.WithHelp("\uea98 a", "archive")),
344 key.NewBinding(key.WithKeys("r"), key.WithHelp("\ue348 r", "refresh")),
345 }
346 if len(m.tabs) > 1 {
347 bindings = append(bindings,
348 key.NewBinding(key.WithKeys("left", "h"), key.WithHelp("←/h", "prev tab")),
349 key.NewBinding(key.WithKeys("right", "l"), key.WithHelp("→/l", "next tab")),
350 )
351 }
352 bindings = append(bindings, m.extraShortHelpKeys...)
353 return bindings
354 }
355
356 l.KeyMap.Quit.SetEnabled(false)
357
358 // Disable default help to render it manually at the bottom
359 l.SetShowHelp(false)
360
361 if m.width > 0 {
362 l.SetWidth(m.width)
363 }
364 if m.height > 0 {
365 l.SetHeight(m.height / 2)
366 }
367
368 // Restore index
369 // If index is out of bounds (e.g. list shrank), clamp it.
370 if currentIndex >= len(items) {
371 currentIndex = len(items) - 1
372 }
373 if currentIndex < 0 {
374 currentIndex = 0
375 }
376 l.Select(currentIndex)
377
378 m.list = l
379}
380
381func (m *Inbox) getTitle() string {
382 var title string
383 if m.currentAccountID == "" {
384 title = m.getBaseTitle() + " - All Accounts"
385 } else {
386 title = m.getBaseTitle()
387 for _, acc := range m.accounts {
388 if acc.ID == m.currentAccountID {
389 if acc.Name != "" {
390 title = fmt.Sprintf("%s - %s", m.getBaseTitle(), acc.Name)
391 } else {
392 title = fmt.Sprintf("%s - %s", m.getBaseTitle(), acc.FetchEmail)
393 }
394 break
395 }
396 }
397 }
398 if m.isRefreshing {
399 title += " (refreshing...)"
400 }
401 if m.isFetching {
402 title += " (loading more...)"
403 }
404 if m.pluginStatus != "" {
405 title += " (" + m.pluginStatus + ")"
406 }
407 return title
408}
409
410func (m *Inbox) getBaseTitle() string {
411 if m.folderName != "" {
412 return m.folderName
413 }
414 switch m.mailbox {
415 case MailboxSent:
416 return "Sent"
417 case MailboxTrash:
418 return "Trash"
419 case MailboxArchive:
420 return "Archive"
421 default:
422 return "Inbox"
423 }
424}
425
426func (m *Inbox) Init() tea.Cmd {
427 return nil
428}
429
430func (m *Inbox) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
431 var cmds []tea.Cmd
432
433 switch msg := msg.(type) {
434 case tea.KeyPressMsg:
435 if m.list.FilterState() == list.Filtering {
436 break
437 }
438 switch keypress := msg.String(); keypress {
439 case "left", "h":
440 if len(m.tabs) > 1 {
441 m.activeTabIndex--
442 if m.activeTabIndex < 0 {
443 m.activeTabIndex = len(m.tabs) - 1
444 }
445 m.currentAccountID = m.tabs[m.activeTabIndex].ID
446 m.updateList()
447 return m, nil
448 }
449 case "right", "l":
450 if len(m.tabs) > 1 {
451 m.activeTabIndex++
452 if m.activeTabIndex >= len(m.tabs) {
453 m.activeTabIndex = 0
454 }
455 m.currentAccountID = m.tabs[m.activeTabIndex].ID
456 m.updateList()
457 return m, nil
458 }
459 case "d":
460 selectedItem, ok := m.list.SelectedItem().(item)
461 if ok {
462 return m, func() tea.Msg {
463 return DeleteEmailMsg{UID: selectedItem.uid, AccountID: selectedItem.accountID, Mailbox: m.mailbox}
464 }
465 }
466 case "a":
467 selectedItem, ok := m.list.SelectedItem().(item)
468 if ok {
469 return m, func() tea.Msg {
470 return ArchiveEmailMsg{UID: selectedItem.uid, AccountID: selectedItem.accountID, Mailbox: m.mailbox}
471 }
472 }
473 case "r":
474 m.isRefreshing = true
475 m.list.Title = m.getTitle()
476 // Copy counts to avoid race conditions if used elsewhere (though here it's just passing data)
477 counts := make(map[string]int)
478 for k, v := range m.emailCountByAcct {
479 counts[k] = v
480 }
481 return m, func() tea.Msg {
482 return RequestRefreshMsg{Mailbox: m.mailbox, Counts: counts}
483 }
484 case "enter":
485 selectedItem, ok := m.list.SelectedItem().(item)
486 if ok {
487 idx := selectedItem.originalIndex
488 uid := selectedItem.uid
489 accountID := selectedItem.accountID
490 return m, func() tea.Msg {
491 return ViewEmailMsg{Index: idx, UID: uid, AccountID: accountID, Mailbox: m.mailbox}
492 }
493 }
494 }
495 case tea.WindowSizeMsg:
496 m.width = msg.Width
497 m.height = msg.Height
498 m.list.SetWidth(msg.Width)
499 m.list.SetHeight(msg.Height / 2)
500 if m.shouldFetchMore() {
501 return m, tea.Batch(m.fetchMoreCmds()...)
502 }
503 return m, nil
504
505 case FetchingMoreEmailsMsg:
506 m.isFetching = true
507 m.list.Title = m.getTitle()
508 return m, nil
509
510 case EmailsAppendedMsg:
511 if msg.Mailbox != m.mailbox {
512 return m, nil
513 }
514 m.isFetching = false
515 m.list.Title = m.getTitle()
516
517 if len(msg.Emails) == 0 {
518 if m.noMoreByAccount == nil {
519 m.noMoreByAccount = make(map[string]bool)
520 }
521 m.noMoreByAccount[msg.AccountID] = true
522 return m, nil
523 }
524
525 // Add emails to the appropriate account
526 for _, email := range msg.Emails {
527 m.emailsByAccount[email.AccountID] = append(m.emailsByAccount[email.AccountID], email)
528 m.allEmails = append(m.allEmails, email)
529 }
530 m.emailCountByAcct[msg.AccountID] = len(m.emailsByAccount[msg.AccountID])
531
532 m.updateList()
533 return m, nil
534
535 case RefreshingEmailsMsg:
536 if msg.Mailbox != m.mailbox {
537 return m, nil
538 }
539 m.isRefreshing = true
540 m.list.Title = m.getTitle()
541 return m, nil
542
543 case EmailsRefreshedMsg:
544 if msg.Mailbox != m.mailbox {
545 return m, nil
546 }
547 // Only clear the refreshing indicator. The actual email data is
548 // merged by the main model (preserving paginated emails) and
549 // pushed to us via SetEmails, so we must not overwrite it here.
550 m.isRefreshing = false
551 m.list.Title = m.getTitle()
552 return m, nil
553 }
554
555 var cmd tea.Cmd
556 m.list, cmd = m.list.Update(msg)
557 cmds = append(cmds, cmd)
558
559 if m.shouldFetchMore() {
560 cmds = append(cmds, m.fetchMoreCmds()...)
561 }
562 return m, tea.Batch(cmds...)
563}
564
565func (m *Inbox) shouldFetchMore() bool {
566 if m.isFetching || m.isRefreshing {
567 return false
568 }
569 if m.allAccountsExhausted() {
570 return false
571 }
572 if len(m.list.Items()) == 0 {
573 return false
574 }
575 if m.list.FilterState() == list.Filtering {
576 return false
577 }
578 // Fetch if we've reached the bottom OR if we don't have enough items to fill the view
579 return m.list.Index() >= len(m.list.Items())-1 || len(m.list.Items()) < m.list.Height()
580}
581
582// allAccountsExhausted returns true if all relevant accounts have no more emails to fetch.
583func (m *Inbox) allAccountsExhausted() bool {
584 if len(m.noMoreByAccount) == 0 {
585 return false
586 }
587 if m.currentAccountID != "" {
588 return m.noMoreByAccount[m.currentAccountID]
589 }
590 // "ALL" view: all accounts must be exhausted
591 for _, acc := range m.accounts {
592 if !m.noMoreByAccount[acc.ID] {
593 return false
594 }
595 }
596 return len(m.accounts) > 0
597}
598
599func (m *Inbox) fetchMoreCmds() []tea.Cmd {
600 var cmds []tea.Cmd
601 limit := uint32(m.list.Height())
602 if limit < 20 {
603 limit = 20
604 }
605
606 if m.currentAccountID == "" {
607 if len(m.accounts) == 0 {
608 return nil
609 }
610 for _, acc := range m.accounts {
611 accountID := acc.ID
612 if m.noMoreByAccount[accountID] {
613 continue
614 }
615 offset := uint32(len(m.emailsByAccount[accountID]))
616 cmds = append(cmds, func(id string, off uint32) tea.Cmd {
617 return func() tea.Msg {
618 return FetchMoreEmailsMsg{Offset: off, AccountID: id, Mailbox: m.mailbox, Limit: limit}
619 }
620 }(accountID, offset))
621 }
622 return cmds
623 }
624
625 if m.noMoreByAccount[m.currentAccountID] {
626 return nil
627 }
628 offset := uint32(len(m.emailsByAccount[m.currentAccountID]))
629 cmds = append(cmds, func(id string, off uint32) tea.Cmd {
630 return func() tea.Msg {
631 return FetchMoreEmailsMsg{Offset: off, AccountID: id, Mailbox: m.mailbox, Limit: limit}
632 }
633 }(m.currentAccountID, offset))
634 return cmds
635}
636
637func (m *Inbox) View() tea.View {
638 var b strings.Builder
639
640 // Render tabs if there are multiple accounts
641 if len(m.tabs) > 1 {
642 var tabViews []string
643 for i, tab := range m.tabs {
644 label := tab.Label
645 if tab.ID == "" {
646 label = "ALL"
647 }
648
649 if i == m.activeTabIndex {
650 tabViews = append(tabViews, activeTabStyle.Render(label))
651 } else {
652 tabViews = append(tabViews, tabStyle.Render(label))
653 }
654 }
655 tabBar := tabBarStyle.Render(lipgloss.JoinHorizontal(lipgloss.Top, tabViews...))
656 b.WriteString(tabBar)
657 b.WriteString("\n")
658 }
659
660 b.WriteString(m.list.View())
661
662 // Ensure we don't start gap calculation on the same line as the list
663 if !strings.HasSuffix(b.String(), "\n") {
664 b.WriteString("\n")
665 }
666
667 helpView := inboxHelpStyle.Render(m.list.Help.View(m.list))
668
669 if m.height > 0 {
670 usedHeight := lipgloss.Height(b.String())
671 helpHeight := lipgloss.Height(helpView)
672
673 gap := m.height - usedHeight - helpHeight
674 if gap > 0 {
675 b.WriteString(strings.Repeat("\n", gap))
676 }
677 } else {
678 b.WriteString("\n")
679 }
680
681 b.WriteString(helpView)
682
683 return tea.NewView(b.String())
684}
685
686// GetCurrentAccountID returns the currently selected account ID
687func (m *Inbox) GetCurrentAccountID() string {
688 return m.currentAccountID
689}
690
691// GetEmailAtIndex returns the email at the given index for the current view
692func (m *Inbox) GetEmailAtIndex(index int) *fetcher.Email {
693 var displayEmails []fetcher.Email
694 if m.currentAccountID == "" {
695 displayEmails = m.allEmails
696 } else {
697 displayEmails = m.emailsByAccount[m.currentAccountID]
698 }
699
700 if index >= 0 && index < len(displayEmails) {
701 return &displayEmails[index]
702 }
703 return nil
704}
705
706func (m *Inbox) GetMailbox() MailboxKind {
707 return m.mailbox
708}
709
710// MarkEmailAsRead marks an email as read by UID and account ID, updating it in all stores.
711func (m *Inbox) MarkEmailAsRead(uid uint32, accountID string) {
712 for i := range m.allEmails {
713 if m.allEmails[i].UID == uid && m.allEmails[i].AccountID == accountID {
714 m.allEmails[i].IsRead = true
715 break
716 }
717 }
718 if emails, ok := m.emailsByAccount[accountID]; ok {
719 for i := range emails {
720 if emails[i].UID == uid {
721 emails[i].IsRead = true
722 break
723 }
724 }
725 }
726 m.updateList()
727}
728
729// RemoveEmail removes an email by UID and account ID
730func (m *Inbox) RemoveEmail(uid uint32, accountID string) {
731 // Remove from account-specific list
732 if emails, ok := m.emailsByAccount[accountID]; ok {
733 var filtered []fetcher.Email
734 for _, e := range emails {
735 if e.UID != uid {
736 filtered = append(filtered, e)
737 }
738 }
739 m.emailsByAccount[accountID] = filtered
740 }
741
742 // Remove from all emails list
743 var filteredAll []fetcher.Email
744 for _, e := range m.allEmails {
745 if !(e.UID == uid && e.AccountID == accountID) {
746 filteredAll = append(filteredAll, e)
747 }
748 }
749 m.allEmails = filteredAll
750
751 m.updateList()
752}
753
754// SetSize sets the width and height of the inbox, then updates the list.
755func (m *Inbox) SetSize(width, height int) {
756 m.width = width
757 m.height = height
758 m.list.SetWidth(width)
759 m.list.SetHeight(height / 2)
760}
761
762// SetFolderName sets a custom folder name for the inbox title.
763func (m *Inbox) SetFolderName(name string) {
764 m.folderName = name
765 m.list.Title = m.getTitle()
766}
767
768// SetPluginStatus sets a persistent status string from plugins, shown in the title.
769func (m *Inbox) SetPluginStatus(status string) {
770 m.pluginStatus = status
771 m.list.Title = m.getTitle()
772}
773
774// SetEmails updates all emails (used after fetch)
775func (m *Inbox) SetEmails(emails []fetcher.Email, accounts []config.Account) {
776 m.accounts = accounts
777 m.allEmails = emails
778 m.noMoreByAccount = make(map[string]bool)
779
780 // Rebuild tabs: empty for single account, "ALL" + accounts for multiple
781 var tabs []AccountTab
782 if len(accounts) <= 1 {
783 tabs = []AccountTab{{ID: "", Label: "", Email: ""}}
784 } else {
785 tabs = []AccountTab{{ID: "", Label: "ALL", Email: ""}}
786 for _, acc := range accounts {
787 tabs = append(tabs, AccountTab{ID: acc.ID, Label: acc.FetchEmail, Email: acc.Email})
788 }
789 }
790 m.tabs = tabs
791
792 // Re-group emails by account
793 m.emailsByAccount = make(map[string][]fetcher.Email)
794 for _, email := range emails {
795 m.emailsByAccount[email.AccountID] = append(m.emailsByAccount[email.AccountID], email)
796 }
797
798 // Update email counts
799 m.emailCountByAcct = make(map[string]int)
800 for accID, accEmails := range m.emailsByAccount {
801 m.emailCountByAcct[accID] = len(accEmails)
802 }
803
804 m.updateList()
805}