~sircmpwn/aerc

Add sorting functionality v3 PROPOSED

Hi Jeffas, greetings to all,

A few questions / comments regarding this patch:

## Why do you make this a separate event?

I think we can get away with doing this in the FetchDirectoryContents
event, else you make 2 round trips instead of a single one if you can do it
server side (only IMAP can do that). Or do I misunderstand?
For the on the fly config change, we could simply emit the same event again.
Other opinions?

## Can we extract the implementation of the sorting from the maildir worker?
IMAP is the only worker which can do something server side and I'd rather avoid
writing the same code twice.
Could you please extract the sorting to a common lib?  Similar to what we
already do with the message parsing in worker/lib/parse.go.
In fact, all you use is the MessageInfo and this is already available via the
"worker/lib.RawMessage" interface and the MessageInfo(RawMessage) function.

## Notmuch

Note, this patch doesn't compile (disregarding the merge conflicts, those are
due to my patches which just got merged)
Export patchset (mbox)
How do I use this?

Copy & paste the following snippet into your terminal to import this patchset into git:

curl -s https://lists.sr.ht/~sircmpwn/aerc/%3C20190916105650.857131-1-dev%40jeffas.io%3E/mbox | git am -3
Learn more about email & git

[PATCH v3 1/2] Add sorting functionality Export this patch

There is a command and config option. The criteria are a list of the
sort criterion and each can be individually reversed.

This only includes support for sorting in the maildir backend currently.
The other backends are not supported in this patch.
---

Add oneOff arg in msgstore sorting function. This ensures that we don't
keep altering the reverse flag for the default criteria.

 commands/account/sort.go |  77 +++++++++++
 config/config.go         |   1 +
 lib/msgstore.go          |  35 ++++-
 lib/sort/sort.go         |  56 ++++++++
 widgets/account.go       |  14 ++
 widgets/msglist.go       |  36 +----
 worker/imap/worker.go    |   2 +
 worker/maildir/sort.go   | 286 +++++++++++++++++++++++++++++++++++++++
 worker/maildir/worker.go |  15 ++
 worker/notmuch/worker.go |   6 +
 worker/types/messages.go |  10 ++
 worker/types/sort.go     |  19 +++
 12 files changed, 525 insertions(+), 32 deletions(-)
 create mode 100644 commands/account/sort.go
 create mode 100644 lib/sort/sort.go
 create mode 100644 worker/maildir/sort.go
 create mode 100644 worker/types/sort.go

diff --git a/commands/account/sort.go b/commands/account/sort.go
new file mode 100644
index 0000000..b1c1722
--- /dev/null
+++ b/commands/account/sort.go
@@ -0,0 +1,77 @@
+package account
+
+import (
+	"errors"
+	"strings"
+
+	"git.sr.ht/~sircmpwn/aerc/lib/sort"
+	"git.sr.ht/~sircmpwn/aerc/widgets"
+)
+
+type Sort struct{}
+
+func init() {
+	register(Sort{})
+}
+
+func (Sort) Aliases() []string {
+	return []string{"sort"}
+}
+
+func (Sort) Complete(aerc *widgets.Aerc, args []string) []string {
+	supportedCriteria := []string{"arrival", "cc", "date",
+		"from", "read", "size", "subject", "to"}
+	if len(args) == 0 {
+		return supportedCriteria
+	}
+	last := args[len(args)-1]
+	var completions []string
+	currentPrefix := strings.Join(args, " ") + " "
+	// if there is a completed criteria then suggest all again or an option
+	for _, criteria := range append(supportedCriteria, "-r") {
+		if criteria == last {
+			for _, criteria := range supportedCriteria {
+				completions = append(completions, currentPrefix+criteria)
+			}
+			return completions
+		}
+	}
+
+	currentPrefix = strings.Join(args[:len(args)-1], " ")
+	if len(args) > 1 {
+		currentPrefix += " "
+	}
+	// last was beginning an option
+	if last == "-" {
+		return []string{currentPrefix + "-r"}
+	}
+	// the last item is not complete
+	for _, criteria := range supportedCriteria {
+		if strings.HasPrefix(criteria, last) {
+			completions = append(completions, currentPrefix+criteria)
+		}
+	}
+	return completions
+}
+
+func (Sort) Execute(aerc *widgets.Aerc, args []string) error {
+	acct := aerc.SelectedAccount()
+	if acct == nil {
+		return errors.New("Cannot perform action. No account selected.")
+	}
+	store := acct.Store()
+	if store == nil {
+		return errors.New("Cannot perform action. Messages still loading.")
+	}
+
+	sortCriteria, err := sort.GetSortCriteria(args[1:])
+	if err != nil {
+		return err
+	}
+
+	aerc.SetStatus("Sorting")
+	store.Sort(sortCriteria, true, func() {
+		aerc.SetStatus("Sorting complete")
+	})
+	return nil
+}
diff --git a/config/config.go b/config/config.go
index eeaf937..5a41903 100644
--- a/config/config.go
+++ b/config/config.go
@@ -36,6 +36,7 @@ type UIConfig struct {
 	Spinner           string   `ini:"spinner"`
 	SpinnerDelimiter  string   `ini:"spinner-delimiter"`
 	DirListFormat     string   `ini:"dirlist-format"`
+	Sort              []string `delim:" "`
 }

 const (
diff --git a/lib/msgstore.go b/lib/msgstore.go
index 73c79e7..ab8375b 100644
--- a/lib/msgstore.go
+++ b/lib/msgstore.go
@@ -13,7 +13,7 @@ type MessageStore struct {
 	Deleted  map[uint32]interface{}
 	DirInfo  models.DirectoryInfo
 	Messages map[uint32]*models.MessageInfo
-	// List of known UIDs, order is not important
+	// List of known UIDs, order is important
 	uids []uint32

 	selected        int
@@ -25,6 +25,9 @@ type MessageStore struct {
 	resultIndex int
 	filter      bool

+	defaultSortCriteria []*types.SortCriterion
+	Sorting             bool
+
 	// Map of uids we've asked the worker to fetch
 	onUpdate       func(store *MessageStore) // TODO: multiple onUpdate handlers
 	onUpdateDirs   func()
@@ -38,8 +41,12 @@ type MessageStore struct {

 func NewMessageStore(worker *types.Worker,
 	dirInfo *models.DirectoryInfo,
+	defaultSortCriteria []*types.SortCriterion,
 	triggerNewEmail func(*models.MessageInfo),
 	triggerDirectoryChange func()) *MessageStore {
+	if len(defaultSortCriteria) > 0 {
+		defaultSortCriteria[0].Reverse = !defaultSortCriteria[0].Reverse
+	}

 	return &MessageStore{
 		Deleted: make(map[uint32]interface{}),
@@ -49,6 +56,8 @@ func NewMessageStore(worker *types.Worker,
 		bodyCallbacks:   make(map[uint32][]func(io.Reader)),
 		headerCallbacks: make(map[uint32][]func(*types.MessageInfo)),

+		defaultSortCriteria: defaultSortCriteria,
+
 		pendingBodies:  make(map[uint32]interface{}),
 		pendingHeaders: make(map[uint32]interface{}),
 		worker:         worker,
@@ -166,6 +175,12 @@ func (store *MessageStore) Update(msg types.WorkerMessage) {
 		store.Messages = newMap
 		store.uids = msg.Uids
 		update = true
+		if store.defaultSortCriteria != nil {
+			store.Sorting = true
+			store.Sort(store.defaultSortCriteria, false, func() {
+				store.Sorting = false
+			})
+		}
 	case *types.MessageInfo:
 		if existing, ok := store.Messages[msg.Info.Uid]; ok && existing != nil {
 			merge(existing, msg.Info)
@@ -220,6 +235,11 @@ func (store *MessageStore) Update(msg types.WorkerMessage) {
 		}
 		store.uids = uids
 		update = true
+	case *types.SortResults:
+		if msg.Uids != nil {
+			store.uids = msg.Uids
+			update = true
+		}
 	}

 	if update {
@@ -434,3 +454,16 @@ func (store *MessageStore) ModifyLabels(uids []uint32, add, remove []string,
 		Remove: remove,
 	}, cb)
 }
+
+func (store *MessageStore) Sort(criteria []*types.SortCriterion, oneOff bool, cb func()) {
+	// Reverse the original decision as the msglist draws the messages in reverse
+	if oneOff && len(criteria) > 0 {
+		criteria[0].Reverse = !criteria[0].Reverse
+	}
+	store.worker.PostAction(&types.SortDirectory{
+		SortCriteria: criteria,
+	}, func(msg types.WorkerMessage) {
+		store.Update(msg)
+		cb()
+	})
+}
diff --git a/lib/sort/sort.go b/lib/sort/sort.go
new file mode 100644
index 0000000..89c36a9
--- /dev/null
+++ b/lib/sort/sort.go
@@ -0,0 +1,56 @@
+package sort
+
+import (
+	"errors"
+	"fmt"
+	"strings"
+
+	"git.sr.ht/~sircmpwn/aerc/worker/types"
+)
+
+func GetSortCriteria(args []string) ([]*types.SortCriterion, error) {
+	var sortCriteria []*types.SortCriterion
+	reverse := false
+	for _, arg := range args {
+		if arg == "-r" {
+			reverse = true
+			continue
+		}
+		field, err := parseSortField(arg)
+		if err != nil {
+			return nil, err
+		}
+		sortCriteria = append(sortCriteria, &types.SortCriterion{
+			Field:   field,
+			Reverse: reverse,
+		})
+		reverse = false
+	}
+	if reverse {
+		return nil, errors.New("Expected argument to reverse")
+	}
+	return sortCriteria, nil
+}
+
+func parseSortField(arg string) (types.SortField, error) {
+	switch strings.ToLower(arg) {
+	case "arrival":
+		return types.SortArrival, nil
+	case "cc":
+		return types.SortCc, nil
+	case "date":
+		return types.SortDate, nil
+	case "from":
+		return types.SortFrom, nil
+	case "read":
+		return types.SortRead, nil
+	case "size":
+		return types.SortSize, nil
+	case "subject":
+		return types.SortSubject, nil
+	case "to":
+		return types.SortTo, nil
+	default:
+		return types.SortArrival, fmt.Errorf("%v is not a valid sort criterion", arg)
+	}
+}
diff --git a/widgets/account.go b/widgets/account.go
index eb6a495..4e8dd17 100644
--- a/widgets/account.go
+++ b/widgets/account.go
@@ -9,6 +9,7 @@ import (

 	"git.sr.ht/~sircmpwn/aerc/config"
 	"git.sr.ht/~sircmpwn/aerc/lib"
+	"git.sr.ht/~sircmpwn/aerc/lib/sort"
 	"git.sr.ht/~sircmpwn/aerc/lib/ui"
 	"git.sr.ht/~sircmpwn/aerc/models"
 	"git.sr.ht/~sircmpwn/aerc/worker"
@@ -218,6 +219,7 @@ func (acct *AccountView) onMessage(msg types.WorkerMessage) {
 			store.Update(msg)
 		} else {
 			store = lib.NewMessageStore(acct.worker, msg.Info,
+				acct.getSortCriteria(),
 				func(msg *models.MessageInfo) {
 					acct.conf.Triggers.ExecNewEmail(acct.acct,
 						acct.conf, msg)
@@ -254,3 +256,15 @@ func (acct *AccountView) onMessage(msg types.WorkerMessage) {
 			Color(tcell.ColorDefault, tcell.ColorRed)
 	}
 }
+
+func (acct *AccountView) getSortCriteria() []*types.SortCriterion {
+	if len(acct.conf.Ui.Sort) == 0 {
+		return nil
+	}
+	criteria, err := sort.GetSortCriteria(acct.conf.Ui.Sort)
+	if err != nil {
+		acct.aerc.PushError(" ui.sort: " + err.Error())
+		return nil
+	}
+	return criteria
+}
diff --git a/widgets/msglist.go b/widgets/msglist.go
index b7c921c..cc1e5fe 100644
--- a/widgets/msglist.go
+++ b/widgets/msglist.go
@@ -3,7 +3,6 @@ package widgets
 import (
 	"fmt"
 	"log"
-	"sort"

 	"github.com/gdamore/tcell"
 	"github.com/mattn/go-runewidth"
@@ -28,34 +27,6 @@ type MessageList struct {
 	aerc          *Aerc
 }

-type msgSorter struct {
-	uids  []uint32
-	store *lib.MessageStore
-}
-
-func (s *msgSorter) Len() int {
-	return len(s.uids)
-}
-
-func (s *msgSorter) Less(i, j int) bool {
-	msgI := s.store.Messages[s.uids[i]]
-	msgJ := s.store.Messages[s.uids[j]]
-	if msgI == nil && msgJ == nil {
-		return false // doesn't matter which order among nulls
-	} else if msgI == nil && msgJ != nil {
-		return true // say i is before j so we sort i to bottom
-	} else if msgI != nil && msgJ == nil {
-		return false // say i is after j so we sort j to bottom
-	}
-	return msgI.InternalDate.Before(msgJ.InternalDate)
-}
-
-func (s *msgSorter) Swap(i, j int) {
-	tmp := s.uids[i]
-	s.uids[i] = s.uids[j]
-	s.uids[j] = tmp
-}
-
 func NewMessageList(conf *config.AercConfig, logger *log.Logger, aerc *Aerc) *MessageList {
 	ml := &MessageList{
 		conf:          conf,
@@ -92,13 +63,16 @@ func (ml *MessageList) Draw(ctx *ui.Context) {
 		}
 	}

+	if store.Sorting {
+		ml.spinner.Draw(ctx)
+		return
+	}
+
 	var (
 		needsHeaders []uint32
 		row          int = 0
 	)
 	uids := store.Uids()
-	sorter := msgSorter{uids: uids, store: store}
-	sort.Stable(&sorter)

 	for i := len(uids) - 1 - ml.scroll; i >= 0; i-- {
 		uid := uids[i]
diff --git a/worker/imap/worker.go b/worker/imap/worker.go
index cd63c39..b4f3f3c 100644
--- a/worker/imap/worker.go
+++ b/worker/imap/worker.go
@@ -178,6 +178,8 @@ func (w *IMAPWorker) handleMessage(msg types.WorkerMessage) error {
 		w.handleAppendMessage(msg)
 	case *types.SearchDirectory:
 		w.handleSearchDirectory(msg)
+	case *types.SortDirectory:
+		return errUnsupported
 	default:
 		return errUnsupported
 	}
diff --git a/worker/maildir/sort.go b/worker/maildir/sort.go
new file mode 100644
index 0000000..4ec390a
--- /dev/null
+++ b/worker/maildir/sort.go
@@ -0,0 +1,286 @@
+package maildir
+
+import (
+	"fmt"
+	"sort"
+	"strings"
+	"time"
+
+	"git.sr.ht/~sircmpwn/aerc/models"
+	"git.sr.ht/~sircmpwn/aerc/worker/types"
+)
+
+func (w *Worker) sortDirectory(criteria []*types.SortCriterion) ([]uint32, error) {
+	uids, err := w.c.UIDs(*w.selected)
+	if err != nil {
+		return nil, err
+	}
+	if len(criteria) == 0 {
+		return nil, nil
+	}
+	// loop through in reverse to ensure we sort by non-primary fields first
+	for i := len(criteria) - 1; i >= 0; i-- {
+		criterion := criteria[i]
+		var err error
+		switch criterion.Field {
+		case types.SortArrival:
+			err = w.sortDate(uids, criterion,
+				func(msgInfo *models.MessageInfo) time.Time {
+					return msgInfo.InternalDate
+				})
+		case types.SortCc:
+			err = w.sortAddresses(uids, criterion,
+				func(msgInfo *models.MessageInfo) []*models.Address {
+					return msgInfo.Envelope.Cc
+				})
+		case types.SortDate:
+			err = w.sortDate(uids, criterion,
+				func(msgInfo *models.MessageInfo) time.Time {
+					return msgInfo.Envelope.Date
+				})
+		case types.SortFrom:
+			err = w.sortAddresses(uids, criterion,
+				func(msgInfo *models.MessageInfo) []*models.Address {
+					return msgInfo.Envelope.From
+				})
+		case types.SortRead:
+			err = w.sortFlags(uids, criterion, models.SeenFlag)
+		case types.SortSize:
+			err = w.sortInts(uids, criterion,
+				func(msgInfo *models.MessageInfo) uint32 {
+					return msgInfo.Size
+				})
+		case types.SortSubject:
+			err = w.sortStrings(uids, criterion,
+				func(msgInfo *models.MessageInfo) string {
+					subject := strings.ToLower(msgInfo.Envelope.Subject)
+					subject = strings.TrimPrefix(subject, "re: ")
+					return strings.TrimPrefix(subject, "fwd: ")
+				})
+		case types.SortTo:
+			err = w.sortAddresses(uids, criterion,
+				func(msgInfo *models.MessageInfo) []*models.Address {
+					return msgInfo.Envelope.To
+				})
+		}
+		if err != nil {
+			return nil, err
+		}
+	}
+	return uids, nil
+}
+
+func (w *Worker) sortDate(uids []uint32, criterion *types.SortCriterion,
+	getValue func(*models.MessageInfo) time.Time) error {
+	var slice []*dateStore
+	for _, uid := range uids {
+		msgInfo, err := w.getMessageInfo(uid)
+		if err != nil {
+			return err
+		}
+		slice = append(slice, &dateStore{
+			Value: getValue(msgInfo),
+			Uid:   uid,
+		})
+	}
+	sortSlice(criterion, dateSlice{slice})
+	for i := 0; i < len(uids); i++ {
+		uids[i] = slice[i].Uid
+	}
+	return nil
+}
+
+func (w *Worker) sortAddresses(uids []uint32, criterion *types.SortCriterion,
+	getValue func(*models.MessageInfo) []*models.Address) error {
+	var slice []*addressStore
+	for _, uid := range uids {
+		msgInfo, err := w.getMessageInfo(uid)
+		if err != nil {
+			return err
+		}
+		slice = append(slice, &addressStore{
+			Value: getValue(msgInfo),
+			Uid:   uid,
+		})
+	}
+	sortSlice(criterion, addressSlice{slice})
+	for i := 0; i < len(uids); i++ {
+		uids[i] = slice[i].Uid
+	}
+	return nil
+}
+
+func (w *Worker) sortFlags(uids []uint32, criterion *types.SortCriterion,
+	testFlag models.Flag) error {
+	var slice []*boolStore
+	for _, uid := range uids {
+		msgInfo, err := w.getMessageInfo(uid)
+		if err != nil {
+			return err
+		}
+		flagPresent := false
+		for _, flag := range msgInfo.Flags {
+			if flag == testFlag {
+				flagPresent = true
+			}
+		}
+		slice = append(slice, &boolStore{
+			Value: flagPresent,
+			Uid:   uid,
+		})
+	}
+	sortSlice(criterion, boolSlice{slice})
+	for i := 0; i < len(uids); i++ {
+		uids[i] = slice[i].Uid
+	}
+	return nil
+}
+
+func (w *Worker) sortInts(uids []uint32, criterion *types.SortCriterion,
+	getValue func(*models.MessageInfo) uint32) error {
+	var slice []*intStore
+	for _, uid := range uids {
+		msgInfo, err := w.getMessageInfo(uid)
+		if err != nil {
+			return err
+		}
+		slice = append(slice, &intStore{
+			Value: getValue(msgInfo),
+			Uid:   uid,
+		})
+	}
+	sortSlice(criterion, intSlice{slice})
+	for i := 0; i < len(uids); i++ {
+		uids[i] = slice[i].Uid
+	}
+	return nil
+}
+
+func (w *Worker) sortStrings(uids []uint32, criterion *types.SortCriterion,
+	getValue func(*models.MessageInfo) string) error {
+	var slice []*lexiStore
+	for _, uid := range uids {
+		msgInfo, err := w.getMessageInfo(uid)
+		if err != nil {
+			return err
+		}
+		slice = append(slice, &lexiStore{
+			Value: getValue(msgInfo),
+			Uid:   uid,
+		})
+	}
+	sortSlice(criterion, lexiSlice{slice})
+	for i := 0; i < len(uids); i++ {
+		uids[i] = slice[i].Uid
+	}
+	return nil
+}
+
+func (w *Worker) getMessageInfo(uid uint32) (*models.MessageInfo, error) {
+	message, err := w.c.Message(*w.selected, uid)
+	if err != nil {
+		return nil, err
+	}
+	msgInfo, err := message.MessageInfo()
+	if err != nil {
+		return nil, err
+	}
+	return msgInfo, nil
+}
+
+type lexiStore struct {
+	Value string
+	Uid   uint32
+}
+
+type lexiSlice struct{ Slice []*lexiStore }
+
+func (s lexiSlice) Len() int      { return len(s.Slice) }
+func (s lexiSlice) Swap(i, j int) { s.Slice[i], s.Slice[j] = s.Slice[j], s.Slice[i] }
+func (s lexiSlice) Less(i, j int) bool {
+	return s.Slice[i].Value < s.Slice[j].Value
+}
+
+type dateStore struct {
+	Value time.Time
+	Uid   uint32
+}
+
+type dateSlice struct{ Slice []*dateStore }
+
+func (s dateSlice) Len() int      { return len(s.Slice) }
+func (s dateSlice) Swap(i, j int) { s.Slice[i], s.Slice[j] = s.Slice[j], s.Slice[i] }
+func (s dateSlice) Less(i, j int) bool {
+	return s.Slice[i].Value.Before(s.Slice[j].Value)
+}
+
+type intStore struct {
+	Value uint32
+	Uid   uint32
+}
+
+type intSlice struct{ Slice []*intStore }
+
+func (s intSlice) Len() int      { return len(s.Slice) }
+func (s intSlice) Swap(i, j int) { s.Slice[i], s.Slice[j] = s.Slice[j], s.Slice[i] }
+func (s intSlice) Less(i, j int) bool {
+	return s.Slice[i].Value < s.Slice[j].Value
+}
+
+type addressStore struct {
+	Value []*models.Address
+	Uid   uint32
+}
+
+type addressSlice struct{ Slice []*addressStore }
+
+func (s addressSlice) Len() int      { return len(s.Slice) }
+func (s addressSlice) Swap(i, j int) { s.Slice[i], s.Slice[j] = s.Slice[j], s.Slice[i] }
+func (s addressSlice) Less(i, j int) bool {
+	addressI, addressJ := s.Slice[i].Value, s.Slice[j].Value
+	var firstI, firstJ *models.Address
+	if len(addressI) > 0 {
+		firstI = addressI[0]
+	}
+	if len(addressJ) > 0 {
+		firstJ = addressJ[0]
+	}
+	if firstI == nil && firstJ == nil {
+		return true
+	} else if firstI == nil && firstJ != nil {
+		return false
+	} else if firstI != nil && firstJ == nil {
+		return true
+	} else /* firstI != nil && firstJ != nil */ {
+		getName := func(addr *models.Address) string {
+			if addr.Name != "" {
+				return addr.Name
+			} else {
+				return fmt.Sprintf("%s@%s", addr.Mailbox, addr.Host)
+			}
+		}
+		return getName(firstI) < getName(firstJ)
+	}
+}
+
+type boolStore struct {
+	Value bool
+	Uid   uint32
+}
+
+type boolSlice struct{ Slice []*boolStore }
+
+func (s boolSlice) Len() int      { return len(s.Slice) }
+func (s boolSlice) Swap(i, j int) { s.Slice[i], s.Slice[j] = s.Slice[j], s.Slice[i] }
+func (s boolSlice) Less(i, j int) bool {
+	valI, valJ := s.Slice[i].Value, s.Slice[j].Value
+	return valI || !valJ
+}
+
+func sortSlice(criterion *types.SortCriterion, interfce sort.Interface) {
+	if criterion.Reverse {
+		sort.Stable(sort.Reverse(interfce))
+	} else {
+		sort.Stable(interfce)
+	}
+}
diff --git a/worker/maildir/worker.go b/worker/maildir/worker.go
index 533bb7c..e2c7de6 100644
--- a/worker/maildir/worker.go
+++ b/worker/maildir/worker.go
@@ -133,6 +133,8 @@ func (w *Worker) handleMessage(msg types.WorkerMessage) error {
 		return w.handleAppendMessage(msg)
 	case *types.SearchDirectory:
 		return w.handleSearchDirectory(msg)
+	case *types.SortDirectory:
+		return w.handleSortDirectory(msg)
 	}
 	return errUnsupported
 }
@@ -409,3 +411,16 @@ func (w *Worker) handleAppendMessage(msg *types.AppendMessage) error {
 func (w *Worker) handleSearchDirectory(msg *types.SearchDirectory) error {
 	return errUnsupported
 }
+
+func (w *Worker) handleSortDirectory(msg *types.SortDirectory) error {
+	w.worker.Logger.Printf("Sorting directory")
+	sortedUids, err := w.sortDirectory(msg.SortCriteria)
+	if err != nil {
+		return err
+	}
+	w.worker.PostMessage(&types.SortResults{
+		Message: types.RespondTo(msg),
+		Uids:    sortedUids,
+	}, nil)
+	return nil
+}
diff --git a/worker/notmuch/worker.go b/worker/notmuch/worker.go
index 58a63ec..a9bc8c5 100644
--- a/worker/notmuch/worker.go
+++ b/worker/notmuch/worker.go
@@ -92,6 +92,8 @@ func (w *worker) handleMessage(msg types.WorkerMessage) error {
 		return w.handleReadMessages(msg)
 	case *types.SearchDirectory:
 		return w.handleSearchDirectory(msg)
+	case *types.SortDirectory:
+		return w.handleSortDirectory(msg)

 		// not implemented, they are generally not used
 		// in a notmuch based workflow
@@ -424,6 +426,10 @@ func (w *worker) handleSearchDirectory(msg *types.SearchDirectory) error {
 	return nil
 }

+func (w *Worker) handleSortDirectory(msg *types.SortDirectory) error {
+	return errUnsupported
+}
+
 func (w *worker) loadQueryMap(acctConfig *config.AccountConfig) error {
 	raw, ok := acctConfig.Params["query-map"]
 	if !ok {
diff --git a/worker/types/messages.go b/worker/types/messages.go
index 9f40b8f..f483119 100644
--- a/worker/types/messages.go
+++ b/worker/types/messages.go
@@ -85,6 +85,11 @@ type SearchDirectory struct {
 	Argv []string
 }

+type SortDirectory struct {
+	Message
+	SortCriteria []*SortCriterion
+}
+
 type CreateDirectory struct {
 	Message
 	Directory string
@@ -156,6 +161,11 @@ type SearchResults struct {
 	Uids []uint32
 }

+type SortResults struct {
+	Message
+	Uids []uint32
+}
+
 type MessageInfo struct {
 	Message
 	Info *models.MessageInfo
diff --git a/worker/types/sort.go b/worker/types/sort.go
new file mode 100644
index 0000000..ffbcf46
--- /dev/null
+++ b/worker/types/sort.go
@@ -0,0 +1,19 @@
+package types
+
+type SortField int
+
+const (
+	SortArrival SortField = iota
+	SortCc
+	SortDate
+	SortFrom
+	SortRead
+	SortSize
+	SortSubject
+	SortTo
+)
+
+type SortCriterion struct {
+	Field   SortField
+	Reverse bool
+}
--
2.23.0
Hi Jeffas, greetings to all,

A few questions / comments regarding this patch:

## Why do you make this a separate event?

I think we can get away with doing this in the FetchDirectoryContents
event, else you make 2 round trips instead of a single one if you can do it
server side (only IMAP can do that). Or do I misunderstand?
For the on the fly config change, we could simply emit the same event again.

Other opinions?

## Can we extract the implementation of the sorting from the maildir worker?

IMAP is the only worker which can do something server side and I'd rather avoid
writing the same code twice.
Could you please extract the sorting to a common lib?  Similar to what we
already do with the message parsing in worker/lib/parse.go.
In fact, all you use is the MessageInfo and this is already available via the
"worker/lib.RawMessage" interface and the MessageInfo(RawMessage) function.

## Notmuch

Note, this patch doesn't compile (disregarding the merge conflicts, those are
due to my patches which just got merged)

[PATCH v3 2/2] Add documentation for sort Export this patch

This adds documentation for the config option and the command.
---
 config/aerc.conf.in   |  9 +++++++++
 doc/aerc-config.5.scd |  9 +++++++++
 doc/aerc.1.scd        | 25 +++++++++++++++++++++++++
 3 files changed, 43 insertions(+)

diff --git a/config/aerc.conf.in b/config/aerc.conf.in
index c50b7b9..9ad7fcd 100644
--- a/config/aerc.conf.in
+++ b/config/aerc.conf.in
@@ -48,6 +48,15 @@ new-message-bell=true
 # Default: %n %>r
 dirlist-format=%n %>r

+# List of space-separated criteria to sort the messages by, see *sort*
+# command in *aerc*(1) for reference. Prefixing a criterion with "-r "
+# reverses that criterion.
+#
+# Example: "from -r date"
+#
+# Default: ""
+sort=
+
 [viewer]
 #
 # Specifies the pager to use when displaying emails. Note that some filters
diff --git a/doc/aerc-config.5.scd b/doc/aerc-config.5.scd
index 91b444a..67ab608 100644
--- a/doc/aerc-config.5.scd
+++ b/doc/aerc-config.5.scd
@@ -125,6 +125,15 @@ These options are configured in the *[ui]* section of aerc.conf.

 	Default: ","

+*sort*
+	List of space-separated criteria to sort the messages by, see *sort*
+	command in *aerc*(1) for reference. Prefixing a criterion with "-r "
+	reverses that criterion.
+
+	Example: "from -r date"
+
+	Default: ""
+
 *dirlist-format*
 	Describes the format string to use for the directory list

diff --git a/doc/aerc.1.scd b/doc/aerc.1.scd
index 2ec17a4..e76b519 100644
--- a/doc/aerc.1.scd
+++ b/doc/aerc.1.scd
@@ -192,6 +192,31 @@ message list, the message in the message viewer, etc).
 	Selects the nth message in the message list (and scrolls it into view if
 	necessary).

+*sort* [[-r] <criterion>]...
+	Sorts the message list by the given criteria. *-r* sorts the
+	immediately following criterion in reverse order.
+
+	Available criteria:
+
+[[ *Criterion*
+:- *Description*
+|  arrival
+:- Date and time of the messages arrival
+|  cc
+:- Addresses in the "cc" field
+|  date
+:- Date and time of the message
+|  from
+:- Addresses in the "from" field
+|  read
+:- Presence of the read flag
+|  size
+:- Size of the message
+|  subject
+:- Subject of the message
+|  to
+:- Addresses in the "to" field
+
 *view*
 	Opens the message viewer to display the selected message.

--
2.23.0
View this thread in the archives