diff --git a/CHANGELOG.md b/CHANGELOG.md index 7919717..615c9c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - View common email envelope headers with `:envelope`. - Notmuch accounts now support maildir operations: `:copy`, `:move`, `:mkdir`, `:rmdir`, `:archive` and the `copy-to` option. +- Display messages from bottom to top with `reverse-msglist-order=true` in + `aerc.conf`. ### Fixed diff --git a/config/aerc.conf b/config/aerc.conf index 384a0db..1650cf1 100644 --- a/config/aerc.conf +++ b/config/aerc.conf @@ -184,6 +184,15 @@ completion-popovers=true #icon-signed-encrypted=✔ #icon-unknown=✘ #icon-invalid=⚠ +# + +# Reverses the order of the message list. By default, the message list is +# ordered with the newest (highest UID) message on top. Reversing the order +# will put the oldest (lowest UID) message on top. This can be useful in cases +# where the backend does not support sorting. +# +# Default: false +#reverse-msglist-order = false #[ui:account=foo] # diff --git a/config/config.go b/config/config.go index 8b8d9d2..8eb4007 100644 --- a/config/config.go +++ b/config/config.go @@ -78,6 +78,8 @@ type UIConfig struct { // customize border appearance BorderCharVertical rune `ini:"-"` BorderCharHorizontal rune `ini:"-"` + + ReverseOrder bool `ini:"reverse-msglist-order"` } type ContextType int diff --git a/doc/aerc-config.5.scd b/doc/aerc-config.5.scd index 310721a..76adaa0 100644 --- a/doc/aerc-config.5.scd +++ b/doc/aerc-config.5.scd @@ -327,6 +327,14 @@ These options are configured in the *[ui]* section of aerc.conf. instances of items /containing/ the string, starting at any position and need not be consecutive characters in the command or option. +*reverse-msglist-order* + Reverses the order of the message list. By default, the message list is + ordered with the newest (highest UID) message on top. Reversing the + order will put the oldest (lowest UID) message on top. This can be + useful in cases where the backend does not support sorting. + + Default: false + *threading-enabled* Enable a threaded view of messages. If this is not supported by the backend (IMAP server or notmuch), threads will be built by the client. diff --git a/lib/msgstore.go b/lib/msgstore.go index 23fbb8f..7b2fbbf 100644 --- a/lib/msgstore.go +++ b/lib/msgstore.go @@ -5,6 +5,7 @@ import ( "sync" "time" + "git.sr.ht/~rjarry/aerc/lib/iterator" "git.sr.ht/~rjarry/aerc/lib/marker" "git.sr.ht/~rjarry/aerc/lib/sort" "git.sr.ht/~rjarry/aerc/lib/ui" @@ -63,6 +64,8 @@ type MessageStore struct { // threads mutex protects the store.threads and store.threadCallback threadsMutex sync.Mutex + + iterFactory iterator.Factory } const MagicUid = 0xFFFFFFFF @@ -71,6 +74,7 @@ func NewMessageStore(worker *types.Worker, dirInfo *models.DirectoryInfo, defaultSortCriteria []*types.SortCriterion, thread bool, clientThreads bool, clientThreadsDelay time.Duration, + reverseOrder bool, triggerNewEmail func(*models.MessageInfo), triggerDirectoryChange func(), ) *MessageStore { @@ -104,6 +108,8 @@ func NewMessageStore(worker *types.Worker, triggerDirectoryChange: triggerDirectoryChange, threadBuilderDelay: clientThreadsDelay, + + iterFactory: iterator.NewFactory(reverseOrder), } } @@ -222,25 +228,23 @@ func (store *MessageStore) Update(msg types.WorkerMessage) { store.runThreadBuilderNow() } case *types.DirectoryThreaded: - var uids []uint32 newMap := make(map[uint32]*models.MessageInfo) - for i := len(msg.Threads) - 1; i >= 0; i-- { - _ = msg.Threads[i].Walk(func(t *types.Thread, level int, currentErr error) error { - uid := t.Uid - uids = append([]uint32{uid}, uids...) - if msg, ok := store.Messages[uid]; ok { - newMap[uid] = msg - } else { - newMap[uid] = nil - directoryChange = true - } - return nil - }) - } - store.Messages = newMap - store.uids = uids + builder := NewThreadBuilder(store.iterFactory) + builder.RebuildUids(msg.Threads) + store.uids = builder.Uids() store.threads = msg.Threads + + for _, uid := range store.uids { + if msg, ok := store.Messages[uid]; ok { + newMap[uid] = msg + } else { + newMap[uid] = nil + directoryChange = true + } + } + + store.Messages = newMap update = true case *types.MessageInfo: if existing, ok := store.Messages[msg.Info.Uid]; ok && existing != nil { @@ -379,6 +383,12 @@ func (store *MessageStore) Threads() []*types.Thread { return store.threads } +func (store *MessageStore) ThreadsIterator() iterator.Iterator { + store.threadsMutex.Lock() + defer store.threadsMutex.Unlock() + return store.iterFactory.NewIterator(store.threads) +} + func (store *MessageStore) ThreadedView() bool { return store.threadedView } @@ -388,6 +398,12 @@ func (store *MessageStore) BuildThreads() bool { } func (store *MessageStore) runThreadBuilder() { + if store.builder == nil { + store.builder = NewThreadBuilder(store.iterFactory) + for _, msg := range store.Messages { + store.builder.Update(msg) + } + } if store.threadBuilderDebounce != nil { if store.threadBuilderDebounce.Stop() { logging.Infof("thread builder debounced") @@ -402,7 +418,7 @@ func (store *MessageStore) runThreadBuilder() { // runThreadBuilderNow runs the threadbuilder without any debounce logic func (store *MessageStore) runThreadBuilderNow() { if store.builder == nil { - store.builder = NewThreadBuilder() + store.builder = NewThreadBuilder(store.iterFactory) for _, msg := range store.Messages { store.builder.Update(msg) } @@ -544,14 +560,18 @@ func (store *MessageStore) Uids() []uint32 { return store.uids } +func (store *MessageStore) UidsIterator() iterator.Iterator { + return store.iterFactory.NewIterator(store.Uids()) +} + func (store *MessageStore) Selected() *models.MessageInfo { return store.Messages[store.selectedUid] } func (store *MessageStore) SelectedUid() uint32 { if store.selectedUid == MagicUid && len(store.Uids()) > 0 { - uids := store.Uids() - store.selectedUid = uids[len(uids)-1] + iter := store.UidsIterator() + store.selectedUid = store.Uids()[iter.StartIndex()] } return store.selectedUid } @@ -573,20 +593,27 @@ func (store *MessageStore) NextPrev(delta int) { if len(uids) == 0 { return } + iter := store.iterFactory.NewIterator(uids) uid := store.SelectedUid() newIdx := store.FindIndexByUid(uid) if newIdx < 0 { - store.Select(uids[len(uids)-1]) + store.Select(uids[iter.StartIndex()]) return } - newIdx -= delta + low, high := iter.EndIndex(), iter.StartIndex() + sign := -1 + if high < low { + low, high = high, low + sign = 1 + } + newIdx += sign * delta if newIdx >= len(uids) { - newIdx = len(uids) - 1 + newIdx = high } else if newIdx < 0 { - newIdx = 0 + newIdx = low } store.Select(uids[newIdx]) diff --git a/lib/threadbuilder.go b/lib/threadbuilder.go index 6cd98e8..75a8079 100644 --- a/lib/threadbuilder.go +++ b/lib/threadbuilder.go @@ -4,6 +4,7 @@ import ( "sync" "time" + "git.sr.ht/~rjarry/aerc/lib/iterator" "git.sr.ht/~rjarry/aerc/logging" "git.sr.ht/~rjarry/aerc/models" "git.sr.ht/~rjarry/aerc/worker/types" @@ -16,13 +17,15 @@ type ThreadBuilder struct { messageidToUid map[string]uint32 seen map[uint32]bool threadedUids []uint32 + iterFactory iterator.Factory } -func NewThreadBuilder() *ThreadBuilder { +func NewThreadBuilder(i iterator.Factory) *ThreadBuilder { tb := &ThreadBuilder{ threadBlocks: make(map[uint32]jwz.Threadable), messageidToUid: make(map[string]uint32), seen: make(map[uint32]bool), + iterFactory: i, } return tb } @@ -154,17 +157,20 @@ func (builder *ThreadBuilder) sortThreads(threads []*types.Thread, orderedUids [ // RebuildUids rebuilds the uids from the given slice of threads func (builder *ThreadBuilder) RebuildUids(threads []*types.Thread) { uids := make([]uint32, 0, len(threads)) - for i := len(threads) - 1; i >= 0; i-- { - _ = threads[i].Walk(func(t *types.Thread, level int, currentErr error) error { - uids = append(uids, t.Uid) - return nil - }) + iterT := builder.iterFactory.NewIterator(threads) + for iterT.Next() { + _ = iterT.Value().(*types.Thread).Walk( + func(t *types.Thread, level int, currentErr error) error { + uids = append(uids, t.Uid) + return nil + }) } - // copy in reverse as msgList displays backwards - for i, j := 0, len(uids)-1; i < j; i, j = i+1, j-1 { - uids[i], uids[j] = uids[j], uids[i] + result := make([]uint32, 0, len(uids)) + iterU := builder.iterFactory.NewIterator(uids) + for iterU.Next() { + result = append(result, iterU.Value().(uint32)) } - builder.threadedUids = uids + builder.threadedUids = result } // threadable implements the jwz.threadable interface which is required for the diff --git a/widgets/account.go b/widgets/account.go index c82646a..1ad149f 100644 --- a/widgets/account.go +++ b/widgets/account.go @@ -285,6 +285,7 @@ func (acct *AccountView) onMessage(msg types.WorkerMessage) { acct.dirlist.UiConfig(name).ThreadingEnabled, acct.dirlist.UiConfig(name).ForceClientThreads, acct.dirlist.UiConfig(name).ClientThreadsDelay, + acct.dirlist.UiConfig(name).ReverseOrder, func(msg *models.MessageInfo) { acct.conf.Triggers.ExecNewEmail(acct.acct, acct.conf, msg) diff --git a/widgets/msglist.go b/widgets/msglist.go index 7857a3d..f53c0b5 100644 --- a/widgets/msglist.go +++ b/widgets/msglist.go @@ -66,11 +66,13 @@ func (ml *MessageList) Draw(ctx *ui.Context) { ml.UpdateScroller(ml.height, len(store.Uids())) if store := ml.Store(); store != nil && len(store.Uids()) > 0 { - idx := store.FindIndexByUid(store.SelectedUid()) - if idx < 0 { - idx = len(store.Uids()) - 1 + iter := store.UidsIterator() + for i := 0; iter.Next(); i++ { + if store.SelectedUid() == iter.Value().(uint32) { + ml.EnsureScroll(i) + break + } } - ml.EnsureScroll(len(store.Uids()) - idx - 1) } textWidth := ctx.Width() @@ -87,23 +89,24 @@ func (ml *MessageList) Draw(ctx *ui.Context) { ) if store.ThreadedView() { - threads := store.Threads() - counter := len(store.Uids()) + iter := store.ThreadsIterator() + var i int = 0 - for i := len(threads) - 1; i >= 0; i-- { + for iter.Next() { + thread := iter.Value().(*types.Thread) var lastSubject string - err := threads[i].Walk(func(t *types.Thread, _ int, currentErr error) error { + err := thread.Walk(func(t *types.Thread, _ int, currentErr error) error { if currentErr != nil { return currentErr } if t.Hidden || t.Deleted { return nil } - counter-- - if counter > len(store.Uids())-1-ml.Scroll() { - // skip messages which are higher than the viewport + if i < ml.Scroll() { + i++ return nil } + i++ msg := store.Messages[t.Uid] var prefix string var subject string @@ -139,9 +142,13 @@ func (ml *MessageList) Draw(ctx *ui.Context) { } } } else { - uids := store.Uids() - for i := len(uids) - 1 - ml.Scroll(); i >= 0; i-- { - uid := uids[i] + iter := store.UidsIterator() + for i := 0; iter.Next(); i++ { + if i < ml.Scroll() { + continue + } + uid := iter.Value().(uint32) + msg := store.Messages[uid] fmtCtx := format.Ctx{ FromAddress: acct.acct.From, @@ -395,13 +402,22 @@ func (ml *MessageList) Select(index int) { if len(uids) == 0 { return } - uidIdx := len(uids) - index - 1 - if uidIdx >= len(store.Uids()) { - uidIdx = 0 - } else if uidIdx < 0 { - uidIdx = len(store.Uids()) - 1 + + iter := store.UidsIterator() + + var uid uint32 + if index < 0 { + uid = uids[iter.EndIndex()] + } else { + uid = uids[iter.StartIndex()] + for i := 0; iter.Next(); i++ { + if i >= index { + uid = iter.Value().(uint32) + break + } + } } - store.Select(store.Uids()[uidIdx]) + store.Select(uid) ml.Invalidate() }