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 sort

sspaeti 74ee2049 4289f663

+68 -28
+4 -14
internal/ui/model.go
··· 2361 2361 func (m *Model) sortEmails() tea.Cmd { 2362 2362 field, rev := m.sortField, m.sortReverse 2363 2363 sort.SliceStable(m.emails, func(i, j int) bool { 2364 - a, b := m.emails[i], m.emails[j] 2365 - var less bool 2366 - switch field { 2367 - case "from": 2368 - less = strings.ToLower(a.From) < strings.ToLower(b.From) 2369 - case "subject": 2370 - less = strings.ToLower(a.Subject) < strings.ToLower(b.Subject) 2371 - case "size": 2372 - less = a.Size < b.Size 2373 - default: // "date" 2374 - less = a.Date.Before(b.Date) 2375 - } 2364 + cmp := compareEmails(m.emails[i], m.emails[j], field) 2365 + // Apply sort direction 2376 2366 if rev { 2377 - return !less 2367 + return cmp > 0 // descending: a > b means a comes first 2378 2368 } 2379 - return less 2369 + return cmp < 0 // ascending: a < b means a comes first 2380 2370 }) 2381 2371 return m.applyFilter() 2382 2372 }
+64 -14
internal/ui/thread.go
··· 11 11 // replyPrefixRe matches common reply/forward prefixes. 12 12 var replyPrefixRe = regexp.MustCompile(`(?i)^(re|fwd?|fw|aw|sv|vs|ref|rif)\s*(\[\d+\])?\s*:\s*`) 13 13 14 + // compareEmails returns -1 if a < b, 0 if a == b, 1 if a > b. 15 + // Comparison uses the specified sortField with deterministic tie-breakers: 16 + // 1. Primary sort field (from/subject/size/date) 17 + // 2. Date (newest first) if primary keys match and sortField != "date" 18 + // 3. UID for fully deterministic ordering 19 + func compareEmails(a, b imap.Email, sortField string) int { 20 + // Primary sort comparison 21 + var cmp int // -1 = a < b, 0 = equal, 1 = a > b 22 + switch sortField { 23 + case "from": 24 + aFrom, bFrom := strings.ToLower(a.From), strings.ToLower(b.From) 25 + if aFrom < bFrom { 26 + cmp = -1 27 + } else if aFrom > bFrom { 28 + cmp = 1 29 + } 30 + case "subject": 31 + aSubj, bSubj := strings.ToLower(a.Subject), strings.ToLower(b.Subject) 32 + if aSubj < bSubj { 33 + cmp = -1 34 + } else if aSubj > bSubj { 35 + cmp = 1 36 + } 37 + case "size": 38 + if a.Size < b.Size { 39 + cmp = -1 40 + } else if a.Size > b.Size { 41 + cmp = 1 42 + } 43 + default: // "date" 44 + if a.Date.Before(b.Date) { 45 + cmp = -1 46 + } else if a.Date.After(b.Date) { 47 + cmp = 1 48 + } 49 + } 50 + 51 + // Tie-breaker 1: date (newest first) if primary keys are equal 52 + if cmp == 0 && sortField != "date" { 53 + if a.Date.After(b.Date) { 54 + cmp = -1 55 + } else if a.Date.Before(b.Date) { 56 + cmp = 1 57 + } 58 + } 59 + 60 + // Tie-breaker 2: UID for deterministic ordering 61 + if cmp == 0 { 62 + if a.UID < b.UID { 63 + cmp = -1 64 + } else if a.UID > b.UID { 65 + cmp = 1 66 + } 67 + } 68 + 69 + return cmp 70 + } 71 + 14 72 // hasReplyPrefix returns true if the subject starts with a reply/forward prefix. 15 73 func hasReplyPrefix(subject string) bool { 16 74 return replyPrefixRe.MatchString(strings.TrimSpace(subject)) ··· 144 202 145 203 // Sort threads by user's chosen sort field and order. 146 204 // We use the newest email in each thread as the representative for sorting. 147 - sort.Slice(threads, func(i, j int) bool { 205 + sort.SliceStable(threads, func(i, j int) bool { 148 206 a := emails[threads[i].newestIdx] 149 207 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 - } 208 + cmp := compareEmails(a, b, sortField) 209 + 210 + // Apply sort direction 161 211 if sortReverse { 162 - return !less 212 + return cmp > 0 // descending: a > b means a comes first 163 213 } 164 - return less 214 + return cmp < 0 // ascending: a < b means a comes first 165 215 }) 166 216 167 217 // Build output with thread connector lines.