aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJason Cox <me@jasoncarloscox.com>2024-02-17 12:34:23 -0500
committerRobin Jarry <robin@jarry.cc>2024-02-26 09:30:35 +0100
commitf3a61a341c81d70b51d91a71e1b0909acdcdb8f4 (patch)
tree58a3156dc63500fd4e0cd0034a59e86cdbb6a3cb
parentd8d5fc8d31f358c6425dfa18a9f8d2767bc6df40 (diff)
downloadaerc-f3a61a341c81d70b51d91a71e1b0909acdcdb8f4.tar.gz
commands: add :query to create named notmuch dirs
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: Create notmuch named queries with the `:query` command. Signed-off-by: Jason Cox <me@jasoncarloscox.com> Tested-by: Inwit <inwit@sindominio.net> Acked-by: Robin Jarry <robin@jarry.cc>
-rw-r--r--app/dirlist.go7
-rw-r--r--app/dirtree.go6
-rw-r--r--commands/account/cf.go44
-rw-r--r--commands/account/mkdir.go2
-rw-r--r--commands/account/query.go67
-rw-r--r--commands/account/rmdir.go4
-rw-r--r--doc/aerc.1.scd12
-rw-r--r--worker/notmuch/eventhandlers.go7
-rw-r--r--worker/notmuch/worker.go60
-rw-r--r--worker/types/messages.go1
10 files changed, 165 insertions, 45 deletions
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 91e75d60..2a20213a 100644
--- a/doc/aerc.1.scd
+++ b/doc/aerc.1.scd
@@ -596,6 +596,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 c5792709..aa2da391 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
@@ -70,6 +72,7 @@ func NewWorker(w *types.Worker) (types.Backend, error) {
Sort: true,
Thread: true,
},
+ dynamicNameQueryMap: make(map[string]string),
}, nil
}
@@ -284,6 +287,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 {
@@ -318,14 +332,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 {
@@ -334,19 +349,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
@@ -355,7 +377,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),
@@ -921,6 +943,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 7ebb3aae..90d3d7bb 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 {