A minimal email TUI where you read with Markdown and write in Neovim. neomd.ssp.sh/docs
email markdown neovim tui
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix sorting

sspaeti 4289f663 c33dda74

+27 -8
+3 -2
internal/ui/inbox.go
··· 237 237 // setEmails replaces the list contents, preserving marked state. 238 238 // It threads emails before display — grouped conversations appear together 239 239 // with tree-drawing prefixes (┌─>) on reply rows. 240 - func setEmails(l *list.Model, emails []imap.Email, marked map[uint32]bool, prefixFolders bool) tea.Cmd { 241 - threaded := threadEmails(emails) 240 + // Sorting respects the user's chosen sortField and sortReverse preferences. 241 + func setEmails(l *list.Model, emails []imap.Email, marked map[uint32]bool, prefixFolders bool, sortField string, sortReverse bool) tea.Cmd { 242 + threaded := threadEmails(emails, sortField, sortReverse) 242 243 items := make([]list.Item, len(threaded)) 243 244 for i, te := range threaded { 244 245 displaySubj := te.email.Subject
+2 -2
internal/ui/model.go
··· 2437 2437 // Call this whenever filterText changes. 2438 2438 func (m *Model) applyFilter() tea.Cmd { 2439 2439 if m.filterText == "" { 2440 - return setEmails(&m.inbox, m.emails, m.markedUIDs, m.shouldPrefixFolderInSubject()) 2440 + return setEmails(&m.inbox, m.emails, m.markedUIDs, m.shouldPrefixFolderInSubject(), m.sortField, m.sortReverse) 2441 2441 } 2442 2442 query := strings.ToLower(m.filterText) 2443 2443 var filtered []imap.Email ··· 2447 2447 filtered = append(filtered, e) 2448 2448 } 2449 2449 } 2450 - return setEmails(&m.inbox, filtered, m.markedUIDs, m.shouldPrefixFolderInSubject()) 2450 + return setEmails(&m.inbox, filtered, m.markedUIDs, m.shouldPrefixFolderInSubject(), m.sortField, m.sortReverse) 2451 2451 } 2452 2452 2453 2453 // handleChord dispatches two-key sequences (g<x>, M<x>, space<x>).
+22 -4
internal/ui/thread.go
··· 46 46 // stay separate. 47 47 // 48 48 // Each thread is sorted internally by date ascending (oldest = root at bottom, 49 - // newest replies on top). Threads are sorted by most recent email. 50 - func threadEmails(emails []imap.Email) []threadedEmail { 49 + // newest replies on top). Threads are sorted by the user's chosen sort field 50 + // and order (sortField: "date", "from", "subject", "size"; sortReverse: true/false). 51 + func threadEmails(emails []imap.Email, sortField string, sortReverse bool) []threadedEmail { 51 52 if len(emails) == 0 { 52 53 return nil 53 54 } ··· 141 142 threads = append(threads, thread{indices: indices, newestIdx: newest}) 142 143 } 143 144 144 - // Sort threads by most recent email (newest first). 145 + // Sort threads by user's chosen sort field and order. 146 + // We use the newest email in each thread as the representative for sorting. 145 147 sort.Slice(threads, func(i, j int) bool { 146 - return emails[threads[i].newestIdx].Date.After(emails[threads[j].newestIdx].Date) 148 + a := emails[threads[i].newestIdx] 149 + b := emails[threads[j].newestIdx] 150 + var less bool 151 + switch sortField { 152 + case "from": 153 + less = strings.ToLower(a.From) < strings.ToLower(b.From) 154 + case "subject": 155 + less = strings.ToLower(a.Subject) < strings.ToLower(b.Subject) 156 + case "size": 157 + less = a.Size < b.Size 158 + default: // "date" 159 + less = a.Date.Before(b.Date) 160 + } 161 + if sortReverse { 162 + return !less 163 + } 164 + return less 147 165 }) 148 166 149 167 // Build output with thread connector lines.