···123123 AutoScreenOnLoad *bool `toml:"auto_screen_on_load"` // screen inbox on every load (default true)
124124 BgSyncInterval int `toml:"bg_sync_interval"` // background sync interval in minutes (0 = disabled, default 5)
125125 BulkProgressThreshold int `toml:"bulk_progress_threshold"` // show progress counter for batches larger than this (default 10)
126126- DraftBackupCount int `toml:"draft_backup_count"` // rolling compose backups in ~/.cache/neomd/drafts/ (default 20, 0 = disabled)
126126+ DraftBackupCount int `toml:"draft_backup_count"` // rolling compose backups in ~/.cache/neomd/drafts/ (default 20, -1 = disabled)
127127}
128128129129-// DraftBackups returns the max number of rolling draft backups (default 20, 0 = disabled).
129129+// DraftBackups returns the max number of rolling draft backups (default 20, -1 = disabled).
130130func (u UIConfig) DraftBackups() int {
131131 if u.DraftBackupCount == 0 {
132132 return 20
+4-30
internal/ui/cmdline.go
···33import (
44 "fmt"
55 "os"
66- "path/filepath"
77- "sort"
86 "strings"
97108 tea "github.com/charmbracelet/bubbletea"
···171169 desc: "reopen the most recent compose backup from ~/.cache/neomd/drafts/",
172170 run: func(m *Model) (tea.Model, tea.Cmd) {
173171 dir := config.DraftsBackupDir()
174174- entries, err := os.ReadDir(dir)
175175- if err != nil || len(entries) == 0 {
176176- m.status = "No draft backups found in " + dir
177177- return m, nil
178178- }
179179- // Sort by mod time descending (newest first).
180180- type fileEntry struct {
181181- path string
182182- modTime int64
183183- }
184184- var files []fileEntry
185185- for _, e := range entries {
186186- if e.IsDir() {
187187- continue
188188- }
189189- info, err := e.Info()
190190- if err != nil {
191191- continue
192192- }
193193- files = append(files, fileEntry{
194194- path: filepath.Join(dir, e.Name()),
195195- modTime: info.ModTime().Unix(),
196196- })
197197- }
172172+ files := listBackupsByAge(dir)
198173 if len(files) == 0 {
199174 m.status = "No draft backups found in " + dir
200175 return m, nil
201176 }
202202- sort.Slice(files, func(i, j int) bool { return files[i].modTime > files[j].modTime })
203177204204- // Read the most recent backup.
205205- raw, err := os.ReadFile(files[0].path)
178178+ // Read the most recent backup (list is oldest-first).
179179+ raw, err := os.ReadFile(files[len(files)-1].path)
206180 if err != nil {
207181 m.status = "read backup: " + err.Error()
208182 m.isError = true
···222196 }
223197 m.compose.step = 3
224198225225- return m.launchEditorWithBodyCmd(to, cc, subject, body)
199199+ return m.launchEditorWithBodyCmd(to, cc, bcc, subject, body)
226200 },
227201 },
228202 {
+32-25
internal/ui/model.go
···167167 return dir
168168}
169169170170+// backupFile holds a backup's full path and modification time.
171171+type backupFile struct {
172172+ path string
173173+ modTime time.Time
174174+}
175175+176176+// listBackupsByAge returns files in dir sorted oldest-first.
177177+func listBackupsByAge(dir string) []backupFile {
178178+ entries, _ := os.ReadDir(dir)
179179+ files := make([]backupFile, 0, len(entries))
180180+ for _, e := range entries {
181181+ if e.IsDir() {
182182+ continue
183183+ }
184184+ info, err := e.Info()
185185+ if err != nil {
186186+ continue
187187+ }
188188+ files = append(files, backupFile{
189189+ path: filepath.Join(dir, e.Name()),
190190+ modTime: info.ModTime(),
191191+ })
192192+ }
193193+ sort.Slice(files, func(i, j int) bool { return files[i].modTime.Before(files[j].modTime) })
194194+ return files
195195+}
196196+170197// backupDraft copies a compose temp file to ~/.cache/neomd/drafts/ before it is
171198// deleted. Keeps at most maxBackups files, pruning the oldest.
172199func backupDraft(tmpPath string, maxBackups int) {
173173- if maxBackups < 0 {
200200+ if maxBackups <= 0 {
174201 return // disabled
175202 }
176203 dir := config.DraftsBackupDir()
···182209 _ = os.WriteFile(dst, src, 0600)
183210184211 // Prune oldest if over limit.
185185- entries, _ := os.ReadDir(dir)
186186- if len(entries) <= maxBackups {
187187- return
188188- }
189189- type fileInfo struct {
190190- name string
191191- modTime time.Time
192192- }
193193- files := make([]fileInfo, 0, len(entries))
194194- for _, e := range entries {
195195- if e.IsDir() {
196196- continue
197197- }
198198- info, err := e.Info()
199199- if err != nil {
200200- continue
201201- }
202202- files = append(files, fileInfo{name: e.Name(), modTime: info.ModTime()})
203203- }
204204- sort.Slice(files, func(i, j int) bool { return files[i].modTime.Before(files[j].modTime) })
212212+ files := listBackupsByAge(dir)
205213 for i := 0; i < len(files)-maxBackups; i++ {
206206- _ = os.Remove(filepath.Join(dir, files[i].name))
214214+ _ = os.Remove(files[i].path)
207215 }
208216}
209217···26522660 if ps.cc != "" || ps.bcc != "" {
26532661 m.compose.extraVisible = true
26542662 }
26552655- return m.launchEditorWithBodyCmd(ps.to, ps.cc, ps.subject, ps.body)
26632663+ return m.launchEditorWithBodyCmd(ps.to, ps.cc, ps.bcc, ps.subject, ps.body)
26562664 case "s":
26572665 // Open in nvim with spell checking, cursor on first error.
26582666 return m.launchSpellCheckCmd(ps)
···28622870// launchEditorWithBodyCmd re-opens the editor with an existing body (e.g. from
28632871// the pre-send screen). The prelude is built from the provided headers (no
28642872// signature — it is already in the body from the first compose).
28652865-func (m Model) launchEditorWithBodyCmd(to, cc, subject, body string) (tea.Model, tea.Cmd) {
28732873+func (m Model) launchEditorWithBodyCmd(to, cc, bcc, subject, body string) (tea.Model, tea.Cmd) {
28662874 prelude := editor.Prelude(to, cc, subject, "")
28672875 content := prelude + body
28682876···28822890 editorBin = "nvim"
28832891 }
2884289228852885- bcc := m.compose.bcc.Value()
28862893 cmd := exec.Command(editorBin, tmpPath)
28872894 draftBackups := m.cfg.UI.DraftBackups()
28882895 return m, tea.ExecProcess(cmd, func(execErr error) tea.Msg {