store: reverse message list order with iterators

Reverse the order of the messages in the message list. The complexity of
reversing the order is abstracted away by the iterators. To reverse the
message list, add the following to your aerc.conf:

[ui]
reverse-msglist-order=true

Thanks to |cos| for sharing his initial implementation of reversing the
order in the message list [0].

[0]: https://git.netizen.se/aerc/commit/?h=topic/asc_sort_imap

Signed-off-by: Koni Marti <koni.marti@gmail.com>
Acked-by: Robin Jarry <robin@jarry.cc>
This commit is contained in:
Koni Marti 2022-10-20 16:43:41 +02:00 committed by Robin Jarry
parent c83ffabf38
commit c5face0b6f
8 changed files with 124 additions and 53 deletions

View File

@ -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`. - View common email envelope headers with `:envelope`.
- Notmuch accounts now support maildir operations: `:copy`, `:move`, `:mkdir`, - Notmuch accounts now support maildir operations: `:copy`, `:move`, `:mkdir`,
`:rmdir`, `:archive` and the `copy-to` option. `:rmdir`, `:archive` and the `copy-to` option.
- Display messages from bottom to top with `reverse-msglist-order=true` in
`aerc.conf`.
### Fixed ### Fixed

View File

@ -184,6 +184,15 @@ completion-popovers=true
#icon-signed-encrypted=✔ #icon-signed-encrypted=✔
#icon-unknown=✘ #icon-unknown=✘
#icon-invalid=⚠ #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] #[ui:account=foo]
# #

View File

@ -78,6 +78,8 @@ type UIConfig struct {
// customize border appearance // customize border appearance
BorderCharVertical rune `ini:"-"` BorderCharVertical rune `ini:"-"`
BorderCharHorizontal rune `ini:"-"` BorderCharHorizontal rune `ini:"-"`
ReverseOrder bool `ini:"reverse-msglist-order"`
} }
type ContextType int type ContextType int

View File

@ -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 instances of items /containing/ the string, starting at any position and
need not be consecutive characters in the command or option. 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* *threading-enabled*
Enable a threaded view of messages. If this is not supported by the 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. backend (IMAP server or notmuch), threads will be built by the client.

View File

@ -5,6 +5,7 @@ import (
"sync" "sync"
"time" "time"
"git.sr.ht/~rjarry/aerc/lib/iterator"
"git.sr.ht/~rjarry/aerc/lib/marker" "git.sr.ht/~rjarry/aerc/lib/marker"
"git.sr.ht/~rjarry/aerc/lib/sort" "git.sr.ht/~rjarry/aerc/lib/sort"
"git.sr.ht/~rjarry/aerc/lib/ui" "git.sr.ht/~rjarry/aerc/lib/ui"
@ -63,6 +64,8 @@ type MessageStore struct {
// threads mutex protects the store.threads and store.threadCallback // threads mutex protects the store.threads and store.threadCallback
threadsMutex sync.Mutex threadsMutex sync.Mutex
iterFactory iterator.Factory
} }
const MagicUid = 0xFFFFFFFF const MagicUid = 0xFFFFFFFF
@ -71,6 +74,7 @@ func NewMessageStore(worker *types.Worker,
dirInfo *models.DirectoryInfo, dirInfo *models.DirectoryInfo,
defaultSortCriteria []*types.SortCriterion, defaultSortCriteria []*types.SortCriterion,
thread bool, clientThreads bool, clientThreadsDelay time.Duration, thread bool, clientThreads bool, clientThreadsDelay time.Duration,
reverseOrder bool,
triggerNewEmail func(*models.MessageInfo), triggerNewEmail func(*models.MessageInfo),
triggerDirectoryChange func(), triggerDirectoryChange func(),
) *MessageStore { ) *MessageStore {
@ -104,6 +108,8 @@ func NewMessageStore(worker *types.Worker,
triggerDirectoryChange: triggerDirectoryChange, triggerDirectoryChange: triggerDirectoryChange,
threadBuilderDelay: clientThreadsDelay, threadBuilderDelay: clientThreadsDelay,
iterFactory: iterator.NewFactory(reverseOrder),
} }
} }
@ -222,25 +228,23 @@ func (store *MessageStore) Update(msg types.WorkerMessage) {
store.runThreadBuilderNow() store.runThreadBuilderNow()
} }
case *types.DirectoryThreaded: case *types.DirectoryThreaded:
var uids []uint32
newMap := make(map[uint32]*models.MessageInfo) newMap := make(map[uint32]*models.MessageInfo)
for i := len(msg.Threads) - 1; i >= 0; i-- { builder := NewThreadBuilder(store.iterFactory)
_ = msg.Threads[i].Walk(func(t *types.Thread, level int, currentErr error) error { builder.RebuildUids(msg.Threads)
uid := t.Uid store.uids = builder.Uids()
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
store.threads = msg.Threads 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 update = true
case *types.MessageInfo: case *types.MessageInfo:
if existing, ok := store.Messages[msg.Info.Uid]; ok && existing != nil { if existing, ok := store.Messages[msg.Info.Uid]; ok && existing != nil {
@ -379,6 +383,12 @@ func (store *MessageStore) Threads() []*types.Thread {
return store.threads 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 { func (store *MessageStore) ThreadedView() bool {
return store.threadedView return store.threadedView
} }
@ -388,6 +398,12 @@ func (store *MessageStore) BuildThreads() bool {
} }
func (store *MessageStore) runThreadBuilder() { 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 != nil {
if store.threadBuilderDebounce.Stop() { if store.threadBuilderDebounce.Stop() {
logging.Infof("thread builder debounced") logging.Infof("thread builder debounced")
@ -402,7 +418,7 @@ func (store *MessageStore) runThreadBuilder() {
// runThreadBuilderNow runs the threadbuilder without any debounce logic // runThreadBuilderNow runs the threadbuilder without any debounce logic
func (store *MessageStore) runThreadBuilderNow() { func (store *MessageStore) runThreadBuilderNow() {
if store.builder == nil { if store.builder == nil {
store.builder = NewThreadBuilder() store.builder = NewThreadBuilder(store.iterFactory)
for _, msg := range store.Messages { for _, msg := range store.Messages {
store.builder.Update(msg) store.builder.Update(msg)
} }
@ -544,14 +560,18 @@ func (store *MessageStore) Uids() []uint32 {
return store.uids return store.uids
} }
func (store *MessageStore) UidsIterator() iterator.Iterator {
return store.iterFactory.NewIterator(store.Uids())
}
func (store *MessageStore) Selected() *models.MessageInfo { func (store *MessageStore) Selected() *models.MessageInfo {
return store.Messages[store.selectedUid] return store.Messages[store.selectedUid]
} }
func (store *MessageStore) SelectedUid() uint32 { func (store *MessageStore) SelectedUid() uint32 {
if store.selectedUid == MagicUid && len(store.Uids()) > 0 { if store.selectedUid == MagicUid && len(store.Uids()) > 0 {
uids := store.Uids() iter := store.UidsIterator()
store.selectedUid = uids[len(uids)-1] store.selectedUid = store.Uids()[iter.StartIndex()]
} }
return store.selectedUid return store.selectedUid
} }
@ -573,20 +593,27 @@ func (store *MessageStore) NextPrev(delta int) {
if len(uids) == 0 { if len(uids) == 0 {
return return
} }
iter := store.iterFactory.NewIterator(uids)
uid := store.SelectedUid() uid := store.SelectedUid()
newIdx := store.FindIndexByUid(uid) newIdx := store.FindIndexByUid(uid)
if newIdx < 0 { if newIdx < 0 {
store.Select(uids[len(uids)-1]) store.Select(uids[iter.StartIndex()])
return 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) { if newIdx >= len(uids) {
newIdx = len(uids) - 1 newIdx = high
} else if newIdx < 0 { } else if newIdx < 0 {
newIdx = 0 newIdx = low
} }
store.Select(uids[newIdx]) store.Select(uids[newIdx])

View File

@ -4,6 +4,7 @@ import (
"sync" "sync"
"time" "time"
"git.sr.ht/~rjarry/aerc/lib/iterator"
"git.sr.ht/~rjarry/aerc/logging" "git.sr.ht/~rjarry/aerc/logging"
"git.sr.ht/~rjarry/aerc/models" "git.sr.ht/~rjarry/aerc/models"
"git.sr.ht/~rjarry/aerc/worker/types" "git.sr.ht/~rjarry/aerc/worker/types"
@ -16,13 +17,15 @@ type ThreadBuilder struct {
messageidToUid map[string]uint32 messageidToUid map[string]uint32
seen map[uint32]bool seen map[uint32]bool
threadedUids []uint32 threadedUids []uint32
iterFactory iterator.Factory
} }
func NewThreadBuilder() *ThreadBuilder { func NewThreadBuilder(i iterator.Factory) *ThreadBuilder {
tb := &ThreadBuilder{ tb := &ThreadBuilder{
threadBlocks: make(map[uint32]jwz.Threadable), threadBlocks: make(map[uint32]jwz.Threadable),
messageidToUid: make(map[string]uint32), messageidToUid: make(map[string]uint32),
seen: make(map[uint32]bool), seen: make(map[uint32]bool),
iterFactory: i,
} }
return tb return tb
} }
@ -154,17 +157,20 @@ func (builder *ThreadBuilder) sortThreads(threads []*types.Thread, orderedUids [
// RebuildUids rebuilds the uids from the given slice of threads // RebuildUids rebuilds the uids from the given slice of threads
func (builder *ThreadBuilder) RebuildUids(threads []*types.Thread) { func (builder *ThreadBuilder) RebuildUids(threads []*types.Thread) {
uids := make([]uint32, 0, len(threads)) uids := make([]uint32, 0, len(threads))
for i := len(threads) - 1; i >= 0; i-- { iterT := builder.iterFactory.NewIterator(threads)
_ = threads[i].Walk(func(t *types.Thread, level int, currentErr error) error { for iterT.Next() {
uids = append(uids, t.Uid) _ = iterT.Value().(*types.Thread).Walk(
return nil func(t *types.Thread, level int, currentErr error) error {
}) uids = append(uids, t.Uid)
return nil
})
} }
// copy in reverse as msgList displays backwards result := make([]uint32, 0, len(uids))
for i, j := 0, len(uids)-1; i < j; i, j = i+1, j-1 { iterU := builder.iterFactory.NewIterator(uids)
uids[i], uids[j] = uids[j], uids[i] 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 // threadable implements the jwz.threadable interface which is required for the

View File

@ -285,6 +285,7 @@ func (acct *AccountView) onMessage(msg types.WorkerMessage) {
acct.dirlist.UiConfig(name).ThreadingEnabled, acct.dirlist.UiConfig(name).ThreadingEnabled,
acct.dirlist.UiConfig(name).ForceClientThreads, acct.dirlist.UiConfig(name).ForceClientThreads,
acct.dirlist.UiConfig(name).ClientThreadsDelay, acct.dirlist.UiConfig(name).ClientThreadsDelay,
acct.dirlist.UiConfig(name).ReverseOrder,
func(msg *models.MessageInfo) { func(msg *models.MessageInfo) {
acct.conf.Triggers.ExecNewEmail(acct.acct, acct.conf.Triggers.ExecNewEmail(acct.acct,
acct.conf, msg) acct.conf, msg)

View File

@ -66,11 +66,13 @@ func (ml *MessageList) Draw(ctx *ui.Context) {
ml.UpdateScroller(ml.height, len(store.Uids())) ml.UpdateScroller(ml.height, len(store.Uids()))
if store := ml.Store(); store != nil && len(store.Uids()) > 0 { if store := ml.Store(); store != nil && len(store.Uids()) > 0 {
idx := store.FindIndexByUid(store.SelectedUid()) iter := store.UidsIterator()
if idx < 0 { for i := 0; iter.Next(); i++ {
idx = len(store.Uids()) - 1 if store.SelectedUid() == iter.Value().(uint32) {
ml.EnsureScroll(i)
break
}
} }
ml.EnsureScroll(len(store.Uids()) - idx - 1)
} }
textWidth := ctx.Width() textWidth := ctx.Width()
@ -87,23 +89,24 @@ func (ml *MessageList) Draw(ctx *ui.Context) {
) )
if store.ThreadedView() { if store.ThreadedView() {
threads := store.Threads() iter := store.ThreadsIterator()
counter := len(store.Uids()) var i int = 0
for i := len(threads) - 1; i >= 0; i-- { for iter.Next() {
thread := iter.Value().(*types.Thread)
var lastSubject string 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 { if currentErr != nil {
return currentErr return currentErr
} }
if t.Hidden || t.Deleted { if t.Hidden || t.Deleted {
return nil return nil
} }
counter-- if i < ml.Scroll() {
if counter > len(store.Uids())-1-ml.Scroll() { i++
// skip messages which are higher than the viewport
return nil return nil
} }
i++
msg := store.Messages[t.Uid] msg := store.Messages[t.Uid]
var prefix string var prefix string
var subject string var subject string
@ -139,9 +142,13 @@ func (ml *MessageList) Draw(ctx *ui.Context) {
} }
} }
} else { } else {
uids := store.Uids() iter := store.UidsIterator()
for i := len(uids) - 1 - ml.Scroll(); i >= 0; i-- { for i := 0; iter.Next(); i++ {
uid := uids[i] if i < ml.Scroll() {
continue
}
uid := iter.Value().(uint32)
msg := store.Messages[uid] msg := store.Messages[uid]
fmtCtx := format.Ctx{ fmtCtx := format.Ctx{
FromAddress: acct.acct.From, FromAddress: acct.acct.From,
@ -395,13 +402,22 @@ func (ml *MessageList) Select(index int) {
if len(uids) == 0 { if len(uids) == 0 {
return return
} }
uidIdx := len(uids) - index - 1
if uidIdx >= len(store.Uids()) { iter := store.UidsIterator()
uidIdx = 0
} else if uidIdx < 0 { var uid uint32
uidIdx = len(store.Uids()) - 1 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() ml.Invalidate()
} }