~rjarry/aerc-devel

aerc: commands: add :query to create named notmuch dirs v1 APPLIED

Jason Cox: 1
 commands: add :query to create named notmuch dirs

 10 files changed, 165 insertions(+), 45 deletions(-)
#1152126 alpine-edge.yml success
#1152127 openbsd.yml success
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/~rjarry/aerc-devel/patches/49601/mbox | git am -3
Learn more about email & git

[PATCH aerc] commands: add :query to create named notmuch dirs Export this patch

The current :cf command can be used to create folders for arbitrary
notmuch queries. These folders use the query as their namee. In some
cases, though, it's useful to give a more human-readable name. Create a
new :query command to allow doing so.

The :query command accepts an optional -n flag to specify a name. The
remaining arguments are interpreted verbatim as a notmuch query. If no
name is specified, the query itself is used as the name.

For example, to create a new folder with the full thread of the current
message, named by its subject, run the following command:

    :query -n "{{.SubjectBase}}" thread:"{mid:{{.MessageId}}}"

:query could have been implemented as an additional flag to :cf. Giving
a name to the created folder would make the smantics of :cf strange,
though. For example, to create a named query folder, one would use
:cf -n <name> <query>. This syntax feels odd; the name of the folder
seems like it ought to be the positional argument of the change folder
command. Alternatively, the usage could be :cf -q <query> <name>, but
this feels wrong as well: the query, which is provided as a positional
parameter when no name is specified, becomes a flag parameter when a
name is specified. What's more, both of these potential usages add a
notmuch-specific flag to an otherwise general command. Creating a new
command feels cleaner. Perhaps the current query functionality of the
:cf command could eventually be deprecated to remove the duplicate
functionality and keep :cf limited to changing to existing folders.

Changelog-added: New :query command to create named notmuch query
 folders.
Signed-off-by: Jason Cox <me@jasoncarloscox.com>
---
 app/dirlist.go                  |  7 ++--
 app/dirtree.go                  |  6 +--
 commands/account/cf.go          | 44 ++++++++++++----------
 commands/account/mkdir.go       |  2 +-
 commands/account/query.go       | 67 +++++++++++++++++++++++++++++++++
 commands/account/rmdir.go       |  4 +-
 doc/aerc.1.scd                  | 12 ++++++
 worker/notmuch/eventhandlers.go |  7 ++++
 worker/notmuch/worker.go        | 60 +++++++++++++++++++++--------
 worker/types/messages.go        |  1 +
 10 files changed, 165 insertions(+), 45 deletions(-)
 create mode 100644 commands/account/query.go

diff --git a/app/dirlist.go b/app/dirlist.go
index 898b8130..1fc2dd63 100644
--- a/app/dirlist.go
+++ b/app/dirlist.go
@@ -24,7 +24,7 @@ type DirectoryLister interface {

	Selected() string
	Select(string)
	Open(string, time.Duration, func(types.WorkerMessage))
	Open(string, string, time.Duration, func(types.WorkerMessage))

	Update(types.WorkerMessage)
	List() []string
@@ -175,10 +175,10 @@ func (dirlist *DirectoryList) ExpandFolder() {
}

func (dirlist *DirectoryList) Select(name string) {
	dirlist.Open(name, dirlist.UiConfig(name).DirListDelay, nil)
	dirlist.Open(name, "", dirlist.UiConfig(name).DirListDelay, nil)
}

func (dirlist *DirectoryList) Open(name string, delay time.Duration,
func (dirlist *DirectoryList) Open(name string, query string, delay time.Duration,
	cb func(types.WorkerMessage),
) {
	dirlist.selecting = name
@@ -193,6 +193,7 @@ func (dirlist *DirectoryList) Open(name string, delay time.Duration,
			dirlist.worker.PostAction(&types.OpenDirectory{
				Context:   ctx,
				Directory: name,
				Query:     query,
			},
				func(msg types.WorkerMessage) {
					switch msg := msg.(type) {
diff --git a/app/dirtree.go b/app/dirtree.go
index 53ab4aad..b735dacd 100644
--- a/app/dirtree.go
+++ b/app/dirtree.go
@@ -239,10 +239,10 @@ func (dt *DirectoryTree) Select(name string) {
	if name == "" {
		return
	}
	dt.Open(name, dt.UiConfig(name).DirListDelay, nil)
	dt.Open(name, "", dt.UiConfig(name).DirListDelay, nil)
}

func (dt *DirectoryTree) Open(name string, delay time.Duration, cb func(types.WorkerMessage)) {
func (dt *DirectoryTree) Open(name string, query string, delay time.Duration, cb func(types.WorkerMessage)) {
	if name == "" {
		return
	}
@@ -252,7 +252,7 @@ func (dt *DirectoryTree) Open(name string, delay time.Duration, cb func(types.Wo
	} else {
		dt.reindex(name)
	}
	dt.DirectoryList.Open(name, delay, func(msg types.WorkerMessage) {
	dt.DirectoryList.Open(name, query, delay, func(msg types.WorkerMessage) {
		if cb != nil {
			cb(msg)
		}
diff --git a/commands/account/cf.go b/commands/account/cf.go
index 2f32e8bc..0f818006 100644
--- a/commands/account/cf.go
+++ b/commands/account/cf.go
@@ -103,35 +103,39 @@ func (c ChangeFolder) Execute([]string) error {
	}

	finalize := func(msg types.WorkerMessage) {
		// As we're waiting for the worker to report status we must run
		// the rest of the actions in this callback.
		switch msg := msg.(type) {
		case *types.Error:
			app.PushError(msg.Error.Error())
		case *types.Done:
			curAccount := app.SelectedAccount()
			previous := curAccount.Directories().Selected()
			history[curAccount.Name()] = previous
			// reset store filtering if we switched folders
			store := acct.Store()
			if store != nil {
				store.ApplyClear()
				acct.SetStatus(state.SearchFilterClear())
			}
			// focus account tab
			acct.Select()
		}
		handleDirOpenResponse(acct, msg)
	}

	if target == "-" {
		if dir, ok := history[acct.Name()]; ok {
			acct.Directories().Open(dir, 0*time.Second, finalize)
			acct.Directories().Open(dir, "", 0*time.Second, finalize)
		} else {
			return errors.New("No previous folder to return to")
		}
	} else {
		acct.Directories().Open(target, 0*time.Second, finalize)
		acct.Directories().Open(target, "", 0*time.Second, finalize)
	}

	return nil
}

func handleDirOpenResponse(acct *app.AccountView, msg types.WorkerMessage) {
	// As we're waiting for the worker to report status we must run
	// the rest of the actions in this callback.
	switch msg := msg.(type) {
	case *types.Error:
		app.PushError(msg.Error.Error())
	case *types.Done:
		curAccount := app.SelectedAccount()
		previous := curAccount.Directories().Selected()
		history[curAccount.Name()] = previous
		// reset store filtering if we switched folders
		store := acct.Store()
		if store != nil {
			store.ApplyClear()
			acct.SetStatus(state.SearchFilterClear())
		}
		// focus account tab
		acct.Select()
	}
}
diff --git a/commands/account/mkdir.go b/commands/account/mkdir.go
index c08c6d4b..9776e8f7 100644
--- a/commands/account/mkdir.go
+++ b/commands/account/mkdir.go
@@ -53,7 +53,7 @@ func (m MakeDir) Execute(args []string) error {
		case *types.Done:
			app.PushStatus("Directory created.", 10*time.Second)
			history[acct.Name()] = previous
			acct.Directories().Open(m.Folder, 0, nil)
			acct.Directories().Open(m.Folder, "", 0, nil)
		case *types.Error:
			app.PushError(msg.Error.Error())
		}
diff --git a/commands/account/query.go b/commands/account/query.go
new file mode 100644
index 00000000..f116d405
--- /dev/null
+++ b/commands/account/query.go
@@ -0,0 +1,67 @@
package account

import (
	"errors"
	"reflect"
	"time"

	"git.sr.ht/~rjarry/aerc/app"
	"git.sr.ht/~rjarry/aerc/commands"
	"git.sr.ht/~rjarry/aerc/worker/handlers"
	"git.sr.ht/~rjarry/aerc/worker/types"
)

type Query struct {
	Account string `opt:"-a" complete:"CompleteAccount"`
	Name    string `opt:"-n"`
	Query   string `opt:"..."`
}

func init() {
	commands.Register(Query{})
}

func (Query) Context() commands.CommandContext {
	return commands.ACCOUNT
}

func (Query) Aliases() []string {
	return []string{"query"}
}

func (Query) CompleteAccount(arg string) []string {
	return commands.FilterList(app.AccountNames(), arg, commands.QuoteSpace)
}

func (q Query) Execute([]string) error {
	var acct *app.AccountView

	if q.Account == "" {
		acct = app.SelectedAccount()
		if acct == nil {
			return errors.New("No account selected")
		}
	} else {
		var err error
		acct, err = app.Account(q.Account)
		if err != nil {
			return err
		}
	}

	notmuch, _ := handlers.GetHandlerForScheme("notmuch", new(types.Worker))
	if reflect.TypeOf(notmuch) != reflect.TypeOf(acct.Worker().Backend) {
		return errors.New(":query is only available for notmuch accounts")
	}

	finalize := func(msg types.WorkerMessage) {
		handleDirOpenResponse(acct, msg)
	}

	name := q.Name
	if name == "" {
		name = q.Query
	}
	acct.Directories().Open(name, q.Query, 0*time.Second, finalize)
	return nil
}
diff --git a/commands/account/rmdir.go b/commands/account/rmdir.go
index 00366bd0..48ea3581 100644
--- a/commands/account/rmdir.go
+++ b/commands/account/rmdir.go
@@ -89,9 +89,9 @@ func (r RemoveDir) Execute(args []string) error {
		return errors.New("No directory to move to afterwards!")
	}

	reopenCurrentDir := func() { acct.Directories().Open(curDir, 0, nil) }
	reopenCurrentDir := func() { acct.Directories().Open(curDir, "", 0, nil) }

	acct.Directories().Open(newDir, 0, func(msg types.WorkerMessage) {
	acct.Directories().Open(newDir, "", 0, func(msg types.WorkerMessage) {
		switch msg.(type) {
		case *types.Done:
			break
diff --git a/doc/aerc.1.scd b/doc/aerc.1.scd
index ae5e98c4..2615dc9b 100644
--- a/doc/aerc.1.scd
+++ b/doc/aerc.1.scd
@@ -605,6 +605,18 @@ message list, the message in the message viewer, etc).
*:prev-result*
	Selects the next or previous search result.

*:query* [*-a* _<account>_] [*-n* _name_] _<notmuch query>_
	Create a virtual folder using the specified top-level notmuch query. This
	command is exclusive to the notmuch backend.

	*-a* _<account>_
		Change to _<folder>_ of _<account>_ and focus its corresponding
		tab.

	*-n* _<name>_
		Specify the display name for the virtual folder. If not provided,
		_<notmuch query>_ is used as the display name.

*:search* [_<options>_] _<terms>_...
	Searches the current folder for messages matching the given set of
	conditions.  The search syntax is dependent on the underlying backend.
diff --git a/worker/notmuch/eventhandlers.go b/worker/notmuch/eventhandlers.go
index be01a71b..76517953 100644
--- a/worker/notmuch/eventhandlers.go
+++ b/worker/notmuch/eventhandlers.go
@@ -55,6 +55,13 @@ func (w *worker) updateDirCounts() error {
		}, nil)
	}

	for name, query := range w.dynamicNameQueryMap {
		w.w.PostMessage(&types.DirectoryInfo{
			Info:    w.getDirectoryInfo(name, query),
			Refetch: w.query == query,
		}, nil)
	}

	return nil
}

diff --git a/worker/notmuch/worker.go b/worker/notmuch/worker.go
index 5f502432..fe41446e 100644
--- a/worker/notmuch/worker.go
+++ b/worker/notmuch/worker.go
@@ -27,6 +27,7 @@ import (
	"git.sr.ht/~rjarry/aerc/worker/lib"
	notmuch "git.sr.ht/~rjarry/aerc/worker/notmuch/lib"
	"git.sr.ht/~rjarry/aerc/worker/types"
	"github.com/emersion/go-maildir"
)

func init() {
@@ -42,6 +43,7 @@ type worker struct {
	currentQueryName    string
	queryMapOrder       []string
	nameQueryMap        map[string]string
	dynamicNameQueryMap map[string]string
	store               *lib.MaildirStore
	maildirAccountPath  string
	db                  *notmuch.DB
@@ -71,6 +73,7 @@ func NewWorker(w *types.Worker) (types.Backend, error) {
			Sort:   true,
			Thread: true,
		},
		dynamicNameQueryMap: make(map[string]string),
	}, nil
}

@@ -295,6 +298,17 @@ func (w *worker) handleListDirectories(msg *types.ListDirectories) error {
			},
		}, nil)
	}

	for name := range w.dynamicNameQueryMap {
		w.w.PostMessage(&types.Directory{
			Message: types.RespondTo(msg),
			Dir: &models.Directory{
				Name: name,
				Role: models.QueryRole,
			},
		}, nil)
	}

	// Update dir counts when listing directories
	err := w.updateDirCounts()
	if err != nil {
@@ -329,14 +343,15 @@ func (w *worker) handleOpenDirectory(msg *types.OpenDirectory) error {
	if msg.Context.Err() != nil {
		return context.Canceled
	}
	w.w.Tracef("opening %s", msg.Directory)
	w.w.Tracef("opening %s with query %s", msg.Directory, msg.Query)

	var isDynamicFolder bool
	var exists bool
	q := ""
	if w.store != nil {
		folders, _ := w.store.FolderMap()
		dir, ok := folders[msg.Directory]
		if ok {
		var dir maildir.Dir
		dir, exists = folders[msg.Directory]
		if exists {
			folder := filepath.Join(w.maildirAccountPath, msg.Directory)
			q = fmt.Sprintf("folder:%s", strconv.Quote(folder))
			if err := w.processNewMaildirFiles(string(dir)); err != nil {
@@ -345,19 +360,26 @@ func (w *worker) handleOpenDirectory(msg *types.OpenDirectory) error {
		}
	}
	if q == "" {
		var ok bool
		q, ok = w.nameQueryMap[msg.Directory]
		if !ok {
		q, exists = w.nameQueryMap[msg.Directory]
		if !exists {
			q, exists = w.dynamicNameQueryMap[msg.Directory]
		}
	}
	if !exists {
		q = msg.Query
		if q == "" {
			q = msg.Directory
			isDynamicFolder = true
			w.w.PostMessage(&types.Directory{
				Message: types.RespondTo(msg),
				Dir: &models.Directory{
					Name: q,
					Role: models.QueryRole,
				},
			}, nil)
		}
		w.dynamicNameQueryMap[msg.Directory] = q
		w.w.PostMessage(&types.Directory{
			Message: types.RespondTo(msg),
			Dir: &models.Directory{
				Name: msg.Directory,
				Role: models.QueryRole,
			},
		}, nil)
	} else if msg.Query != "" && msg.Query != q {
		return errors.New("cannot use existing folder name for new query")
	}
	w.query = q
	w.currentQueryName = msg.Directory
@@ -366,7 +388,7 @@ func (w *worker) handleOpenDirectory(msg *types.OpenDirectory) error {
		Info:    w.getDirectoryInfo(msg.Directory, w.query),
		Message: types.RespondTo(msg),
	}, nil)
	if isDynamicFolder {
	if !exists {
		w.w.PostMessage(&types.DirectoryInfo{
			Info:    w.getDirectoryInfo(msg.Directory, w.query),
			Message: types.RespondTo(msg),
@@ -927,6 +949,12 @@ func (w *worker) handleRemoveDirectory(msg *types.RemoveDirectory) error {
		return errUnsupported
	}

	if _, ok := w.dynamicNameQueryMap[msg.Directory]; ok {
		delete(w.dynamicNameQueryMap, msg.Directory)
		w.done(msg)
		return nil
	}

	if w.store == nil {
		w.done(msg)
		return nil
diff --git a/worker/types/messages.go b/worker/types/messages.go
index 3639177e..bbc430ca 100644
--- a/worker/types/messages.go
+++ b/worker/types/messages.go
@@ -101,6 +101,7 @@ type OpenDirectory struct {
	Message
	Context   context.Context
	Directory string
	Query     string
}

type FetchDirectoryContents struct {
-- 
2.43.1
Jason Cox <me@jasoncarloscox.com> wrote:
aerc/patches: SUCCESS in 2m14s

[commands: add :query to create named notmuch dirs][0] from [Jason Cox][1]

[0]: https://lists.sr.ht/~rjarry/aerc-devel/patches/49601
[1]: mailto:me@jasoncarloscox.com

✓ #1152127 SUCCESS aerc/patches/openbsd.yml     https://builds.sr.ht/~rjarry/job/1152127
✓ #1152126 SUCCESS aerc/patches/alpine-edge.yml https://builds.sr.ht/~rjarry/job/1152126
Tested-By: inwit <inwit@sindominio.net> 

And I agree with your take on a new command and even deprecating :cf's
query functionality.