threading: enable filtering of server-side threads
This patch enables the filtering of a threaded view which uses server-built threads. Filtering is done server-side, in order to preserve the use of server-built threads. In adding this feature, the filtering of notmuch folders was brought up to feature parity with the other workers. The filters function the same (ie: they can be stacked). The notmuch filters, however, still use notmuch syntax for the filtering. Signed-off-by: Tim Culverhouse <tim@timculverhouse.com> Acked-by: Robin Jarry <robin@jarry.cc>
This commit is contained in:
parent
ccd042889f
commit
c2f4404fca
10 changed files with 102 additions and 76 deletions
|
@ -6,6 +6,7 @@ import (
|
||||||
|
|
||||||
"git.sr.ht/~rjarry/aerc/lib/statusline"
|
"git.sr.ht/~rjarry/aerc/lib/statusline"
|
||||||
"git.sr.ht/~rjarry/aerc/widgets"
|
"git.sr.ht/~rjarry/aerc/widgets"
|
||||||
|
"git.sr.ht/~rjarry/aerc/worker/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SearchFilter struct{}
|
type SearchFilter struct{}
|
||||||
|
@ -32,27 +33,29 @@ func (SearchFilter) Execute(aerc *widgets.Aerc, args []string) error {
|
||||||
return errors.New("Cannot perform action. Messages still loading")
|
return errors.New("Cannot perform action. Messages still loading")
|
||||||
}
|
}
|
||||||
|
|
||||||
var cb func([]uint32)
|
|
||||||
if args[0] == "filter" {
|
if args[0] == "filter" {
|
||||||
if len(args[1:]) == 0 {
|
if len(args[1:]) == 0 {
|
||||||
return Clear{}.Execute(aerc, []string{"clear"})
|
return Clear{}.Execute(aerc, []string{"clear"})
|
||||||
}
|
}
|
||||||
acct.SetStatus(statusline.FilterActivity("Filtering..."), statusline.Search(""))
|
acct.SetStatus(statusline.FilterActivity("Filtering..."), statusline.Search(""))
|
||||||
cb = func(uids []uint32) {
|
store.SetFilter(args[1:])
|
||||||
acct.SetStatus(statusline.FilterResult(strings.Join(args, " ")))
|
cb := func(msg types.WorkerMessage) {
|
||||||
acct.Logger().Printf("Filter results: %v", uids)
|
if _, ok := msg.(*types.Done); ok {
|
||||||
store.ApplyFilter(uids)
|
acct.SetStatus(statusline.FilterResult(strings.Join(args, " ")))
|
||||||
|
acct.Logger().Printf("Filter results: %v", store.Uids())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
store.Sort(nil, cb)
|
||||||
} else {
|
} else {
|
||||||
acct.SetStatus(statusline.Search("Searching..."))
|
acct.SetStatus(statusline.Search("Searching..."))
|
||||||
cb = func(uids []uint32) {
|
cb := func(uids []uint32) {
|
||||||
acct.SetStatus(statusline.Search(strings.Join(args, " ")))
|
acct.SetStatus(statusline.Search(strings.Join(args, " ")))
|
||||||
acct.Logger().Printf("Search results: %v", uids)
|
acct.Logger().Printf("Search results: %v", uids)
|
||||||
store.ApplySearch(uids)
|
store.ApplySearch(uids)
|
||||||
// TODO: Remove when stores have multiple OnUpdate handlers
|
// TODO: Remove when stores have multiple OnUpdate handlers
|
||||||
acct.Messages().Invalidate()
|
acct.Messages().Invalidate()
|
||||||
}
|
}
|
||||||
|
store.Search(args, cb)
|
||||||
}
|
}
|
||||||
store.Search(args, cb)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -84,8 +84,10 @@ func (Sort) Execute(aerc *widgets.Aerc, args []string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
acct.SetStatus(statusline.Sorting(true))
|
acct.SetStatus(statusline.Sorting(true))
|
||||||
store.Sort(sortCriteria, func() {
|
store.Sort(sortCriteria, func(msg types.WorkerMessage) {
|
||||||
acct.SetStatus(statusline.Sorting(false))
|
if _, ok := msg.(*types.Done); ok {
|
||||||
|
acct.SetStatus(statusline.Sorting(false))
|
||||||
|
}
|
||||||
})
|
})
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -74,10 +74,14 @@ in notmuch, like :delete and :archive.++
|
||||||
Others are slightly different in semantics and mentioned below:
|
Others are slightly different in semantics and mentioned below:
|
||||||
|
|
||||||
*cf* <notmuch query>
|
*cf* <notmuch query>
|
||||||
The change folder command allows for arbitrary notmuch queries and should
|
The change folder command allows for arbitrary notmuch queries. Performing a
|
||||||
usually be preferred over *:filter* as it will be much faster if you use
|
cf command will perform a new top-level notmuch query
|
||||||
the notmuch database to do the filtering
|
|
||||||
|
|
||||||
|
*filter* <notmuch query>
|
||||||
|
The filter command for notmuch backends takes in arbitrary notmuch queries.
|
||||||
|
It applies the query on the set of messages shown in the message list. This
|
||||||
|
can be used to perform successive filters/queries. It is equivalent to
|
||||||
|
performing a set of queries concatenated with "and"
|
||||||
|
|
||||||
# SEE ALSO
|
# SEE ALSO
|
||||||
|
|
||||||
|
|
|
@ -33,8 +33,7 @@ type MessageStore struct {
|
||||||
// Search/filter results
|
// Search/filter results
|
||||||
results []uint32
|
results []uint32
|
||||||
resultIndex int
|
resultIndex int
|
||||||
filtered []uint32
|
filter []string
|
||||||
filter bool
|
|
||||||
|
|
||||||
sortCriteria []*types.SortCriterion
|
sortCriteria []*types.SortCriterion
|
||||||
|
|
||||||
|
@ -83,6 +82,7 @@ func NewMessageStore(worker *types.Worker,
|
||||||
threadedView: thread,
|
threadedView: thread,
|
||||||
buildThreads: clientThreads,
|
buildThreads: clientThreads,
|
||||||
|
|
||||||
|
filter: []string{"filter"},
|
||||||
sortCriteria: defaultSortCriteria,
|
sortCriteria: defaultSortCriteria,
|
||||||
|
|
||||||
pendingBodies: make(map[uint32]interface{}),
|
pendingBodies: make(map[uint32]interface{}),
|
||||||
|
@ -215,7 +215,6 @@ func (store *MessageStore) Update(msg types.WorkerMessage) {
|
||||||
}
|
}
|
||||||
store.Messages = newMap
|
store.Messages = newMap
|
||||||
store.uids = msg.Uids
|
store.uids = msg.Uids
|
||||||
sort.SortBy(store.filtered, store.uids)
|
|
||||||
store.checkMark()
|
store.checkMark()
|
||||||
update = true
|
update = true
|
||||||
case *types.DirectoryThreaded:
|
case *types.DirectoryThreaded:
|
||||||
|
@ -313,14 +312,6 @@ func (store *MessageStore) Update(msg types.WorkerMessage) {
|
||||||
}
|
}
|
||||||
store.results = newResults
|
store.results = newResults
|
||||||
|
|
||||||
var newFiltered []uint32
|
|
||||||
for _, res := range store.filtered {
|
|
||||||
if _, deleted := toDelete[res]; !deleted {
|
|
||||||
newFiltered = append(newFiltered, res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
store.filtered = newFiltered
|
|
||||||
|
|
||||||
for _, thread := range store.Threads {
|
for _, thread := range store.Threads {
|
||||||
thread.Walk(func(t *types.Thread, _ int, _ error) error {
|
thread.Walk(func(t *types.Thread, _ int, _ error) error {
|
||||||
if _, deleted := toDelete[t.Uid]; deleted {
|
if _, deleted := toDelete[t.Uid]; deleted {
|
||||||
|
@ -392,13 +383,7 @@ func (store *MessageStore) runThreadBuilder() {
|
||||||
store.builder.Update(msg)
|
store.builder.Update(msg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var uids []uint32
|
store.Threads = store.builder.Threads(store.uids)
|
||||||
if store.filter {
|
|
||||||
uids = store.filtered
|
|
||||||
} else {
|
|
||||||
uids = store.uids
|
|
||||||
}
|
|
||||||
store.Threads = store.builder.Threads(uids)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (store *MessageStore) Delete(uids []uint32,
|
func (store *MessageStore) Delete(uids []uint32,
|
||||||
|
@ -496,10 +481,6 @@ func (store *MessageStore) Uids() []uint32 {
|
||||||
return uids
|
return uids
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if store.filter {
|
|
||||||
return store.filtered
|
|
||||||
}
|
|
||||||
return store.uids
|
return store.uids
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -732,30 +713,16 @@ func (store *MessageStore) ApplySearch(results []uint32) {
|
||||||
store.NextResult()
|
store.NextResult()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (store *MessageStore) ApplyFilter(results []uint32) {
|
func (store *MessageStore) SetFilter(args []string) {
|
||||||
defer store.Reselect(store.Selected())
|
store.filter = append(store.filter, args...)
|
||||||
store.results = nil
|
|
||||||
store.filtered = results
|
|
||||||
store.filter = true
|
|
||||||
if store.onFilterChange != nil {
|
|
||||||
store.onFilterChange(store)
|
|
||||||
}
|
|
||||||
store.update()
|
|
||||||
// any marking is now invalid
|
|
||||||
// TODO: could save that probably
|
|
||||||
store.ClearVisualMark()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (store *MessageStore) ApplyClear() {
|
func (store *MessageStore) ApplyClear() {
|
||||||
store.results = nil
|
store.filter = []string{"filter"}
|
||||||
store.filtered = nil
|
|
||||||
store.filter = false
|
|
||||||
if store.BuildThreads() {
|
|
||||||
store.runThreadBuilder()
|
|
||||||
}
|
|
||||||
if store.onFilterChange != nil {
|
if store.onFilterChange != nil {
|
||||||
store.onFilterChange(store)
|
store.onFilterChange(store)
|
||||||
}
|
}
|
||||||
|
store.Sort(nil, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (store *MessageStore) nextPrevResult(delta int) {
|
func (store *MessageStore) nextPrevResult(delta int) {
|
||||||
|
@ -796,24 +763,30 @@ func (store *MessageStore) ModifyLabels(uids []uint32, add, remove []string,
|
||||||
}, cb)
|
}, cb)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (store *MessageStore) Sort(criteria []*types.SortCriterion, cb func()) {
|
func (store *MessageStore) Sort(criteria []*types.SortCriterion, cb func(types.WorkerMessage)) {
|
||||||
|
if criteria == nil {
|
||||||
|
criteria = store.sortCriteria
|
||||||
|
} else {
|
||||||
|
store.sortCriteria = criteria
|
||||||
|
}
|
||||||
store.Sorting = true
|
store.Sorting = true
|
||||||
store.sortCriteria = criteria
|
|
||||||
|
|
||||||
handle_return := func(msg types.WorkerMessage) {
|
handle_return := func(msg types.WorkerMessage) {
|
||||||
store.Sorting = false
|
store.Sorting = false
|
||||||
if cb != nil {
|
if cb != nil {
|
||||||
cb()
|
cb(msg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if store.threadedView && !store.buildThreads {
|
if store.threadedView && !store.buildThreads {
|
||||||
store.worker.PostAction(&types.FetchDirectoryThreaded{
|
store.worker.PostAction(&types.FetchDirectoryThreaded{
|
||||||
SortCriteria: criteria,
|
SortCriteria: criteria,
|
||||||
|
FilterCriteria: store.filter,
|
||||||
}, handle_return)
|
}, handle_return)
|
||||||
} else {
|
} else {
|
||||||
store.worker.PostAction(&types.FetchDirectoryContents{
|
store.worker.PostAction(&types.FetchDirectoryContents{
|
||||||
SortCriteria: criteria,
|
SortCriteria: criteria,
|
||||||
|
FilterCriteria: store.filter,
|
||||||
}, handle_return)
|
}, handle_return)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -191,7 +191,11 @@ func (dirlist *DirectoryList) Select(name string) {
|
||||||
}
|
}
|
||||||
dirlist.sortDirsByFoldersSortConfig()
|
dirlist.sortDirsByFoldersSortConfig()
|
||||||
if newStore {
|
if newStore {
|
||||||
dirlist.worker.PostAction(&types.FetchDirectoryContents{}, nil)
|
store, ok := dirlist.MsgStore(name)
|
||||||
|
if ok {
|
||||||
|
// Fetch directory contents via store.Sort
|
||||||
|
store.Sort(nil, nil)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
dirlist.Invalidate()
|
dirlist.Invalidate()
|
||||||
|
|
|
@ -3,7 +3,6 @@ package imap
|
||||||
import (
|
import (
|
||||||
"sort"
|
"sort"
|
||||||
|
|
||||||
"github.com/emersion/go-imap"
|
|
||||||
sortthread "github.com/emersion/go-imap-sortthread"
|
sortthread "github.com/emersion/go-imap-sortthread"
|
||||||
|
|
||||||
"git.sr.ht/~rjarry/aerc/worker/types"
|
"git.sr.ht/~rjarry/aerc/worker/types"
|
||||||
|
@ -29,11 +28,13 @@ func (imapw *IMAPWorker) handleFetchDirectoryContents(
|
||||||
|
|
||||||
imapw.worker.Logger.Printf("Fetching UID list")
|
imapw.worker.Logger.Printf("Fetching UID list")
|
||||||
|
|
||||||
seqSet := &imap.SeqSet{}
|
searchCriteria, err := parseSearch(msg.FilterCriteria)
|
||||||
seqSet.AddRange(1, imapw.selected.Messages)
|
if err != nil {
|
||||||
|
imapw.worker.PostMessage(&types.Error{
|
||||||
searchCriteria := &imap.SearchCriteria{
|
Message: types.RespondTo(msg),
|
||||||
SeqNum: seqSet,
|
Error: err,
|
||||||
|
}, nil)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
sortCriteria := translateSortCriterions(msg.SortCriteria)
|
sortCriteria := translateSortCriterions(msg.SortCriteria)
|
||||||
|
|
||||||
|
@ -98,10 +99,16 @@ func (imapw *IMAPWorker) handleDirectoryThreaded(
|
||||||
msg *types.FetchDirectoryThreaded) {
|
msg *types.FetchDirectoryThreaded) {
|
||||||
imapw.worker.Logger.Printf("Fetching threaded UID list")
|
imapw.worker.Logger.Printf("Fetching threaded UID list")
|
||||||
|
|
||||||
seqSet := &imap.SeqSet{}
|
searchCriteria, err := parseSearch(msg.FilterCriteria)
|
||||||
seqSet.AddRange(1, imapw.selected.Messages)
|
if err != nil {
|
||||||
|
imapw.worker.PostMessage(&types.Error{
|
||||||
|
Message: types.RespondTo(msg),
|
||||||
|
Error: err,
|
||||||
|
}, nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
threads, err := imapw.client.thread.UidThread(sortthread.References,
|
threads, err := imapw.client.thread.UidThread(sortthread.References,
|
||||||
&imap.SearchCriteria{SeqNum: seqSet})
|
searchCriteria)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
imapw.worker.PostMessage(&types.Error{
|
imapw.worker.PostMessage(&types.Error{
|
||||||
Message: types.RespondTo(msg),
|
Message: types.RespondTo(msg),
|
||||||
|
|
|
@ -11,6 +11,9 @@ import (
|
||||||
|
|
||||||
func parseSearch(args []string) (*imap.SearchCriteria, error) {
|
func parseSearch(args []string) (*imap.SearchCriteria, error) {
|
||||||
criteria := imap.NewSearchCriteria()
|
criteria := imap.NewSearchCriteria()
|
||||||
|
if len(args) == 0 {
|
||||||
|
return criteria, nil
|
||||||
|
}
|
||||||
|
|
||||||
opts, optind, err := getopt.Getopts(args, "rubax:X:t:H:f:c:")
|
opts, optind, err := getopt.Getopts(args, "rubax:X:t:H:f:c:")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -406,10 +406,25 @@ func (w *Worker) handleOpenDirectory(msg *types.OpenDirectory) error {
|
||||||
|
|
||||||
func (w *Worker) handleFetchDirectoryContents(
|
func (w *Worker) handleFetchDirectoryContents(
|
||||||
msg *types.FetchDirectoryContents) error {
|
msg *types.FetchDirectoryContents) error {
|
||||||
uids, err := w.c.UIDs(*w.selected)
|
var (
|
||||||
if err != nil {
|
uids []uint32
|
||||||
w.worker.Logger.Printf("error scanning uids: %v", err)
|
err error
|
||||||
return err
|
)
|
||||||
|
if len(msg.FilterCriteria) > 0 {
|
||||||
|
filter, err := parseSearch(msg.FilterCriteria)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
uids, err = w.search(filter)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
uids, err = w.c.UIDs(*w.selected)
|
||||||
|
if err != nil {
|
||||||
|
w.worker.Logger.Printf("error scanning uids: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
sortedUids, err := w.sort(uids, msg.SortCriteria)
|
sortedUids, err := w.sort(uids, msg.SortCriteria)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -550,7 +550,14 @@ func (w *worker) loadExcludeTags(
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *worker) emitDirectoryContents(parent types.WorkerMessage) error {
|
func (w *worker) emitDirectoryContents(parent types.WorkerMessage) error {
|
||||||
uids, err := w.uidsFromQuery(w.query)
|
query := w.query
|
||||||
|
if msg, ok := parent.(*types.FetchDirectoryContents); ok {
|
||||||
|
s := strings.Join(msg.FilterCriteria[1:], " ")
|
||||||
|
if s != "" {
|
||||||
|
query = fmt.Sprintf("(%v) and (%v)", query, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
uids, err := w.uidsFromQuery(query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("could not fetch uids: %v", err)
|
return fmt.Errorf("could not fetch uids: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -567,12 +574,18 @@ func (w *worker) emitDirectoryContents(parent types.WorkerMessage) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *worker) emitDirectoryThreaded(parent types.WorkerMessage) error {
|
func (w *worker) emitDirectoryThreaded(parent types.WorkerMessage) error {
|
||||||
threads, err := w.db.ThreadsFromQuery(w.query)
|
query := w.query
|
||||||
|
if msg, ok := parent.(*types.FetchDirectoryThreaded); ok {
|
||||||
|
s := strings.Join(msg.FilterCriteria[1:], " ")
|
||||||
|
if s != "" {
|
||||||
|
query = fmt.Sprintf("(%v) and (%v)", query, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
threads, err := w.db.ThreadsFromQuery(query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
w.w.PostMessage(&types.DirectoryThreaded{
|
w.w.PostMessage(&types.DirectoryThreaded{
|
||||||
Message: types.RespondTo(parent),
|
|
||||||
Threads: threads,
|
Threads: threads,
|
||||||
}, nil)
|
}, nil)
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -87,12 +87,14 @@ type OpenDirectory struct {
|
||||||
|
|
||||||
type FetchDirectoryContents struct {
|
type FetchDirectoryContents struct {
|
||||||
Message
|
Message
|
||||||
SortCriteria []*SortCriterion
|
SortCriteria []*SortCriterion
|
||||||
|
FilterCriteria []string
|
||||||
}
|
}
|
||||||
|
|
||||||
type FetchDirectoryThreaded struct {
|
type FetchDirectoryThreaded struct {
|
||||||
Message
|
Message
|
||||||
SortCriteria []*SortCriterion
|
SortCriteria []*SortCriterion
|
||||||
|
FilterCriteria []string
|
||||||
}
|
}
|
||||||
|
|
||||||
type SearchDirectory struct {
|
type SearchDirectory struct {
|
||||||
|
|
Loading…
Reference in a new issue