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 roborev suggestions

sspaeti e0736f2f 779462c8

+38 -57
+2 -2
internal/config/config.go
··· 123 123 AutoScreenOnLoad *bool `toml:"auto_screen_on_load"` // screen inbox on every load (default true) 124 124 BgSyncInterval int `toml:"bg_sync_interval"` // background sync interval in minutes (0 = disabled, default 5) 125 125 BulkProgressThreshold int `toml:"bulk_progress_threshold"` // show progress counter for batches larger than this (default 10) 126 - DraftBackupCount int `toml:"draft_backup_count"` // rolling compose backups in ~/.cache/neomd/drafts/ (default 20, 0 = disabled) 126 + DraftBackupCount int `toml:"draft_backup_count"` // rolling compose backups in ~/.cache/neomd/drafts/ (default 20, -1 = disabled) 127 127 } 128 128 129 - // DraftBackups returns the max number of rolling draft backups (default 20, 0 = disabled). 129 + // DraftBackups returns the max number of rolling draft backups (default 20, -1 = disabled). 130 130 func (u UIConfig) DraftBackups() int { 131 131 if u.DraftBackupCount == 0 { 132 132 return 20
+4 -30
internal/ui/cmdline.go
··· 3 3 import ( 4 4 "fmt" 5 5 "os" 6 - "path/filepath" 7 - "sort" 8 6 "strings" 9 7 10 8 tea "github.com/charmbracelet/bubbletea" ··· 171 169 desc: "reopen the most recent compose backup from ~/.cache/neomd/drafts/", 172 170 run: func(m *Model) (tea.Model, tea.Cmd) { 173 171 dir := config.DraftsBackupDir() 174 - entries, err := os.ReadDir(dir) 175 - if err != nil || len(entries) == 0 { 176 - m.status = "No draft backups found in " + dir 177 - return m, nil 178 - } 179 - // Sort by mod time descending (newest first). 180 - type fileEntry struct { 181 - path string 182 - modTime int64 183 - } 184 - var files []fileEntry 185 - for _, e := range entries { 186 - if e.IsDir() { 187 - continue 188 - } 189 - info, err := e.Info() 190 - if err != nil { 191 - continue 192 - } 193 - files = append(files, fileEntry{ 194 - path: filepath.Join(dir, e.Name()), 195 - modTime: info.ModTime().Unix(), 196 - }) 197 - } 172 + files := listBackupsByAge(dir) 198 173 if len(files) == 0 { 199 174 m.status = "No draft backups found in " + dir 200 175 return m, nil 201 176 } 202 - sort.Slice(files, func(i, j int) bool { return files[i].modTime > files[j].modTime }) 203 177 204 - // Read the most recent backup. 205 - raw, err := os.ReadFile(files[0].path) 178 + // Read the most recent backup (list is oldest-first). 179 + raw, err := os.ReadFile(files[len(files)-1].path) 206 180 if err != nil { 207 181 m.status = "read backup: " + err.Error() 208 182 m.isError = true ··· 222 196 } 223 197 m.compose.step = 3 224 198 225 - return m.launchEditorWithBodyCmd(to, cc, subject, body) 199 + return m.launchEditorWithBodyCmd(to, cc, bcc, subject, body) 226 200 }, 227 201 }, 228 202 {
+32 -25
internal/ui/model.go
··· 167 167 return dir 168 168 } 169 169 170 + // backupFile holds a backup's full path and modification time. 171 + type backupFile struct { 172 + path string 173 + modTime time.Time 174 + } 175 + 176 + // listBackupsByAge returns files in dir sorted oldest-first. 177 + func listBackupsByAge(dir string) []backupFile { 178 + entries, _ := os.ReadDir(dir) 179 + files := make([]backupFile, 0, len(entries)) 180 + for _, e := range entries { 181 + if e.IsDir() { 182 + continue 183 + } 184 + info, err := e.Info() 185 + if err != nil { 186 + continue 187 + } 188 + files = append(files, backupFile{ 189 + path: filepath.Join(dir, e.Name()), 190 + modTime: info.ModTime(), 191 + }) 192 + } 193 + sort.Slice(files, func(i, j int) bool { return files[i].modTime.Before(files[j].modTime) }) 194 + return files 195 + } 196 + 170 197 // backupDraft copies a compose temp file to ~/.cache/neomd/drafts/ before it is 171 198 // deleted. Keeps at most maxBackups files, pruning the oldest. 172 199 func backupDraft(tmpPath string, maxBackups int) { 173 - if maxBackups < 0 { 200 + if maxBackups <= 0 { 174 201 return // disabled 175 202 } 176 203 dir := config.DraftsBackupDir() ··· 182 209 _ = os.WriteFile(dst, src, 0600) 183 210 184 211 // Prune oldest if over limit. 185 - entries, _ := os.ReadDir(dir) 186 - if len(entries) <= maxBackups { 187 - return 188 - } 189 - type fileInfo struct { 190 - name string 191 - modTime time.Time 192 - } 193 - files := make([]fileInfo, 0, len(entries)) 194 - for _, e := range entries { 195 - if e.IsDir() { 196 - continue 197 - } 198 - info, err := e.Info() 199 - if err != nil { 200 - continue 201 - } 202 - files = append(files, fileInfo{name: e.Name(), modTime: info.ModTime()}) 203 - } 204 - sort.Slice(files, func(i, j int) bool { return files[i].modTime.Before(files[j].modTime) }) 212 + files := listBackupsByAge(dir) 205 213 for i := 0; i < len(files)-maxBackups; i++ { 206 - _ = os.Remove(filepath.Join(dir, files[i].name)) 214 + _ = os.Remove(files[i].path) 207 215 } 208 216 } 209 217 ··· 2652 2660 if ps.cc != "" || ps.bcc != "" { 2653 2661 m.compose.extraVisible = true 2654 2662 } 2655 - return m.launchEditorWithBodyCmd(ps.to, ps.cc, ps.subject, ps.body) 2663 + return m.launchEditorWithBodyCmd(ps.to, ps.cc, ps.bcc, ps.subject, ps.body) 2656 2664 case "s": 2657 2665 // Open in nvim with spell checking, cursor on first error. 2658 2666 return m.launchSpellCheckCmd(ps) ··· 2862 2870 // launchEditorWithBodyCmd re-opens the editor with an existing body (e.g. from 2863 2871 // the pre-send screen). The prelude is built from the provided headers (no 2864 2872 // signature — it is already in the body from the first compose). 2865 - func (m Model) launchEditorWithBodyCmd(to, cc, subject, body string) (tea.Model, tea.Cmd) { 2873 + func (m Model) launchEditorWithBodyCmd(to, cc, bcc, subject, body string) (tea.Model, tea.Cmd) { 2866 2874 prelude := editor.Prelude(to, cc, subject, "") 2867 2875 content := prelude + body 2868 2876 ··· 2882 2890 editorBin = "nvim" 2883 2891 } 2884 2892 2885 - bcc := m.compose.bcc.Value() 2886 2893 cmd := exec.Command(editorBin, tmpPath) 2887 2894 draftBackups := m.cfg.UI.DraftBackups() 2888 2895 return m, tea.ExecProcess(cmd, func(execErr error) tea.Msg {