~sircmpwn/aerc

Add sorting functionality v4 PROPOSED

Jeffas: 2
 Add sorting functionality
 Add documentation for sort

 12 files changed, 534 insertions(+), 8 deletions(-)
These patches look great, thanks for all of your hard work! I've
incorporated Reto's feedback, made some minor tweaks of my own, and
pushed this:

To git.sr.ht:~sircmpwn/aerc
   43435ba..36af93b  master -> master

Cheers!
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/patches/8301/mbox | git am -3
Learn more about email & git

[PATCH v4 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.
---

Moved sorting to happen in FetchDirectoryContents as this can avoid
another round trip, especially useful for IMAP.

Moved sorting logic to worker/lib/sort.go so that other backends can use
it if needed and changed the main sort function there to take a slice of
the messageInfos as that is what is required and is best to leave this
to the backends to obtain.

Reverted notmuch changes and unnecessary IMAP ones too in those workers.

Many thanks to Reto for the comments!

 commands/account/sort.go |  85 +++++++++++++
 config/config.go         |   1 +
 lib/msgstore.go          |  17 ++-
 lib/sort/sort.go         |  56 +++++++++
 widgets/account.go       |  14 +++
 worker/lib/sort.go       | 253 +++++++++++++++++++++++++++++++++++++++
 worker/maildir/worker.go |  53 ++++++--
 worker/types/messages.go |   1 +
 worker/types/sort.go     |  19 +++
 9 files changed, 491 insertions(+), 8 deletions(-)
 create mode 100644 commands/account/sort.go
 create mode 100644 lib/sort/sort.go
 create mode 100644 worker/lib/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..2449d77
--- /dev/null
+++ b/commands/account/sort.go
@@ -0,0 +1,85 @@
+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, 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 1f18fbf..b0392ba 100644
--- a/lib/msgstore.go
+++ b/lib/msgstore.go
@@ -25,6 +25,8 @@ type MessageStore struct {
 	resultIndex int
 	filter      bool

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

 func NewMessageStore(worker *types.Worker,
 	dirInfo *models.DirectoryInfo,
+	defaultSortCriteria []*types.SortCriterion,
 	triggerNewEmail func(*models.MessageInfo),
 	triggerDirectoryChange func()) *MessageStore {

@@ -49,6 +52,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,
@@ -151,7 +156,9 @@ func (store *MessageStore) Update(msg types.WorkerMessage) {
 	switch msg := msg.(type) {
 	case *types.DirectoryInfo:
 		store.DirInfo = *msg.Info
-		store.worker.PostAction(&types.FetchDirectoryContents{}, nil)
+		store.worker.PostAction(&types.FetchDirectoryContents{
+			SortCriteria: store.defaultSortCriteria,
+		}, nil)
 		update = true
 	case *types.DirectoryContents:
 		newMap := make(map[uint32]*models.MessageInfo)
@@ -434,3 +441,11 @@ func (store *MessageStore) ModifyLabels(uids []uint32, add, remove []string,
 		Remove: remove,
 	}, cb)
 }
+
+func (store *MessageStore) Sort(criteria []*types.SortCriterion, cb func()) {
+	store.worker.PostAction(&types.FetchDirectoryContents{
+		SortCriteria: criteria,
+	}, func(msg types.WorkerMessage) {
+		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/worker/lib/sort.go b/worker/lib/sort.go
new file mode 100644
index 0000000..36c0924
--- /dev/null
+++ b/worker/lib/sort.go
@@ -0,0 +1,253 @@
+package lib
+
+import (
+	"fmt"
+	"sort"
+	"strings"
+	"time"
+
+	"git.sr.ht/~sircmpwn/aerc/models"
+	"git.sr.ht/~sircmpwn/aerc/worker/types"
+)
+
+func Sort(messageInfos []*models.MessageInfo,
+	criteria []*types.SortCriterion) ([]uint32, error) {
+	// 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 = sortDate(messageInfos, criterion,
+				func(msgInfo *models.MessageInfo) time.Time {
+					return msgInfo.InternalDate
+				})
+		case types.SortCc:
+			err = sortAddresses(messageInfos, criterion,
+				func(msgInfo *models.MessageInfo) []*models.Address {
+					return msgInfo.Envelope.Cc
+				})
+		case types.SortDate:
+			err = sortDate(messageInfos, criterion,
+				func(msgInfo *models.MessageInfo) time.Time {
+					return msgInfo.Envelope.Date
+				})
+		case types.SortFrom:
+			err = sortAddresses(messageInfos, criterion,
+				func(msgInfo *models.MessageInfo) []*models.Address {
+					return msgInfo.Envelope.From
+				})
+		case types.SortRead:
+			err = sortFlags(messageInfos, criterion, models.SeenFlag)
+		case types.SortSize:
+			err = sortInts(messageInfos, criterion,
+				func(msgInfo *models.MessageInfo) uint32 {
+					return msgInfo.Size
+				})
+		case types.SortSubject:
+			err = sortStrings(messageInfos, 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 = sortAddresses(messageInfos, criterion,
+				func(msgInfo *models.MessageInfo) []*models.Address {
+					return msgInfo.Envelope.To
+				})
+		}
+		if err != nil {
+			return nil, err
+		}
+	}
+	var uids []uint32
+	// copy in reverse as msgList displays backwards
+	for i := len(messageInfos) - 1; i >= 0; i-- {
+		uids = append(uids, messageInfos[i].Uid)
+	}
+	return uids, nil
+}
+
+func sortDate(messageInfos []*models.MessageInfo, criterion *types.SortCriterion,
+	getValue func(*models.MessageInfo) time.Time) error {
+	var slice []*dateStore
+	for _, msgInfo := range messageInfos {
+		slice = append(slice, &dateStore{
+			Value:   getValue(msgInfo),
+			MsgInfo: msgInfo,
+		})
+	}
+	sortSlice(criterion, dateSlice{slice})
+	for i := 0; i < len(messageInfos); i++ {
+		messageInfos[i] = slice[i].MsgInfo
+	}
+	return nil
+}
+
+func sortAddresses(messageInfos []*models.MessageInfo, criterion *types.SortCriterion,
+	getValue func(*models.MessageInfo) []*models.Address) error {
+	var slice []*addressStore
+	for _, msgInfo := range messageInfos {
+		slice = append(slice, &addressStore{
+			Value:   getValue(msgInfo),
+			MsgInfo: msgInfo,
+		})
+	}
+	sortSlice(criterion, addressSlice{slice})
+	for i := 0; i < len(messageInfos); i++ {
+		messageInfos[i] = slice[i].MsgInfo
+	}
+	return nil
+}
+
+func sortFlags(messageInfos []*models.MessageInfo, criterion *types.SortCriterion,
+	testFlag models.Flag) error {
+	var slice []*boolStore
+	for _, msgInfo := range messageInfos {
+		flagPresent := false
+		for _, flag := range msgInfo.Flags {
+			if flag == testFlag {
+				flagPresent = true
+			}
+		}
+		slice = append(slice, &boolStore{
+			Value:   flagPresent,
+			MsgInfo: msgInfo,
+		})
+	}
+	sortSlice(criterion, boolSlice{slice})
+	for i := 0; i < len(messageInfos); i++ {
+		messageInfos[i] = slice[i].MsgInfo
+	}
+	return nil
+}
+
+func sortInts(messageInfos []*models.MessageInfo, criterion *types.SortCriterion,
+	getValue func(*models.MessageInfo) uint32) error {
+	var slice []*intStore
+	for _, msgInfo := range messageInfos {
+		slice = append(slice, &intStore{
+			Value:   getValue(msgInfo),
+			MsgInfo: msgInfo,
+		})
+	}
+	sortSlice(criterion, intSlice{slice})
+	for i := 0; i < len(messageInfos); i++ {
+		messageInfos[i] = slice[i].MsgInfo
+	}
+	return nil
+}
+
+func sortStrings(messageInfos []*models.MessageInfo, criterion *types.SortCriterion,
+	getValue func(*models.MessageInfo) string) error {
+	var slice []*lexiStore
+	for _, msgInfo := range messageInfos {
+		slice = append(slice, &lexiStore{
+			Value:   getValue(msgInfo),
+			MsgInfo: msgInfo,
+		})
+	}
+	sortSlice(criterion, lexiSlice{slice})
+	for i := 0; i < len(messageInfos); i++ {
+		messageInfos[i] = slice[i].MsgInfo
+	}
+	return nil
+}
+
+type lexiStore struct {
+	Value   string
+	MsgInfo *models.MessageInfo
+}
+
+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
+	MsgInfo *models.MessageInfo
+}
+
+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
+	MsgInfo *models.MessageInfo
+}
+
+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
+	MsgInfo *models.MessageInfo
+}
+
+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 false
+	} 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
+	MsgInfo *models.MessageInfo
+}
+
+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 597e0d2..33c9345 100644
--- a/worker/maildir/worker.go
+++ b/worker/maildir/worker.go
@@ -12,6 +12,7 @@ import (

 	"git.sr.ht/~sircmpwn/aerc/models"
 	"git.sr.ht/~sircmpwn/aerc/worker/handlers"
+	"git.sr.ht/~sircmpwn/aerc/worker/lib"
 	"git.sr.ht/~sircmpwn/aerc/worker/types"
 )

@@ -23,11 +24,12 @@ var errUnsupported = fmt.Errorf("unsupported command")

 // A Worker handles interfacing between aerc's UI and a group of maildirs.
 type Worker struct {
-	c            *Container
-	selected     *maildir.Dir
-	selectedName string
-	worker       *types.Worker
-	watcher      *fsnotify.Watcher
+	c                   *Container
+	selected            *maildir.Dir
+	selectedName        string
+	worker              *types.Worker
+	watcher             *fsnotify.Watcher
+	currentSortCriteria []*types.SortCriterion
 }

 // NewWorker creates a new maildir worker with the provided worker.
@@ -86,8 +88,13 @@ func (w *Worker) handleFSEvent(ev fsnotify.Event) {
 		w.worker.Logger.Printf("could not scan UIDs: %v", err)
 		return
 	}
+	sortedUids, err := w.sort(uids, w.currentSortCriteria)
+	if err != nil {
+		w.worker.Logger.Printf("error sorting directory: %v", err)
+		return
+	}
 	w.worker.PostMessage(&types.DirectoryContents{
-		Uids: uids,
+		Uids: sortedUids,
 	}, nil)
 	dirInfo := w.getDirectoryInfo()
 	dirInfo.Recent = len(newUnseen)
@@ -271,13 +278,45 @@ func (w *Worker) handleFetchDirectoryContents(
 		w.worker.Logger.Printf("error scanning uids: %v", err)
 		return err
 	}
+	sortedUids, err := w.sort(uids, msg.SortCriteria)
+	if err != nil {
+		w.worker.Logger.Printf("error sorting directory: %v", err)
+		return err
+	}
+	w.currentSortCriteria = msg.SortCriteria
 	w.worker.PostMessage(&types.DirectoryContents{
 		Message: types.RespondTo(msg),
-		Uids:    uids,
+		Uids:    sortedUids,
 	}, nil)
 	return nil
 }

+func (w *Worker) sort(uids []uint32, criteria []*types.SortCriterion) ([]uint32, error) {
+	if len(criteria) > 0 {
+		var msgInfos []*models.MessageInfo
+		for _, uid := range uids {
+			m, err := w.c.Message(*w.selected, uid)
+			if err != nil {
+				w.worker.Logger.Printf("could not get message: %v", err)
+				continue
+			}
+			info, err := m.MessageInfo()
+			if err != nil {
+				w.worker.Logger.Printf("could not get message info: %v", err)
+				continue
+			}
+			msgInfos = append(msgInfos, info)
+		}
+		sortedUids, err := lib.Sort(msgInfos, criteria)
+		if err != nil {
+			w.worker.Logger.Printf("could not sort the messages: %v", err)
+			return nil, err
+		}
+		return sortedUids, nil
+	}
+	return uids, nil
+}
+
 func (w *Worker) handleCreateDirectory(msg *types.CreateDirectory) error {
 	dir := w.c.Dir(msg.Directory)
 	if err := dir.Create(); err != nil {
diff --git a/worker/types/messages.go b/worker/types/messages.go
index 9f40b8f..3539139 100644
--- a/worker/types/messages.go
+++ b/worker/types/messages.go
@@ -78,6 +78,7 @@ type OpenDirectory struct {

 type FetchDirectoryContents struct {
 	Message
+	SortCriteria []*SortCriterion
 }

 type SearchDirectory struct {
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
Thanks a lot for the work!

Minor nit, can you return early here?

On Thu, Sep 19, 2019 at 11:37:44PM +0100, Jeffas wrote:

[PATCH v4 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
These patches look great, thanks for all of your hard work! I've
incorporated Reto's feedback, made some minor tweaks of my own, and
pushed this:

To git.sr.ht:~sircmpwn/aerc
   43435ba..36af93b  master -> master

Cheers!
View this thread in the archives