~sircmpwn/aerc

Add sorting functionality v1 PROPOSED

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/%3C20190916080440.322424-1-dev%40jeffas.io%3E/mbox | git am -3
Learn more about email & git

[PATCH 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.
---
 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   | 287 +++++++++++++++++++++++++++++++++++++++
 worker/maildir/worker.go |  15 ++
 worker/notmuch/worker.go |   6 +
 worker/types/messages.go |  10 ++
 worker/types/sort.go     |  19 +++
 12 files changed, 523 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..b375e3a
--- /dev/null
+++ b/worker/maildir/sort.go
@@ -0,0 +1,287 @@
+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: ")
+					w.worker.Logger.Printf("%v, %v", msgInfo.Uid, msgInfo.Envelope.Subject)
+					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 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