~sircmpwn/aerc

Add sorting functionality v2 PROPOSED

Jeffas: 2
 Add sorting functionality
 Add documentation for sort

 15 files changed, 565 insertions(+), 32 deletions(-)
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/8184/mbox | git am -3
Learn more about email & git

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

Fixed Less functions in maildir worker sorting.

Also removed a debug print.

 commands/account/sort.go |  77 +++++++++++
 config/config.go         |   1 +
 lib/msgstore.go          |  32 ++++-
 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, 522 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..d7fefad
--- /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, 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..151a9d7 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,6 +41,7 @@ type MessageStore struct {

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

@@ -49,6 +53,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 +172,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, func() {
				store.Sorting = false
			})
		}
	case *types.MessageInfo:
		if existing, ok := store.Messages[msg.Info.Uid]; ok && existing != nil {
			merge(existing, msg.Info)
@@ -220,6 +232,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 +451,16 @@ func (store *MessageStore) ModifyLabels(uids []uint32, add, remove []string,
		Remove: remove,
	}, cb)
}

func (store *MessageStore) Sort(criteria []*types.SortCriterion, cb func()) {
	// Reverse the original decision as the msglist draws the messages in reverse
	if 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

[PATCH v2 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