aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--commands/commands.go2
-rw-r--r--commands/menu.go187
-rw-r--r--config/aerc.conf15
-rw-r--r--config/general.go1
-rw-r--r--doc/aerc-config.5.scd11
-rw-r--r--doc/aerc.1.scd50
-rw-r--r--lib/ui/box.go1
7 files changed, 266 insertions, 1 deletions
diff --git a/commands/commands.go b/commands/commands.go
index fa8ef287..b617bade 100644
--- a/commands/commands.go
+++ b/commands/commands.go
@@ -113,7 +113,7 @@ func (err NoSuchCommand) Error() string {
// im --> import-mbox
func ExpandAbbreviations(name string) (string, Command, error) {
context := CurrentContext()
- name = strings.TrimLeft(name, ":")
+ name = strings.TrimLeft(name, ": \t")
cmd, found := allCommands[name]
if found && cmd.Context()&context != 0 {
diff --git a/commands/menu.go b/commands/menu.go
new file mode 100644
index 00000000..953c6b45
--- /dev/null
+++ b/commands/menu.go
@@ -0,0 +1,187 @@
+package commands
+
+import (
+ "errors"
+ "io"
+ "os"
+ "os/exec"
+ "strings"
+
+ "git.sr.ht/~rjarry/aerc/app"
+ "git.sr.ht/~rjarry/aerc/config"
+ "git.sr.ht/~rjarry/aerc/lib/ui"
+ "git.sr.ht/~rjarry/aerc/log"
+ "git.sr.ht/~rjarry/aerc/models"
+ "git.sr.ht/~rjarry/go-opt"
+)
+
+type Menu struct {
+ ErrExit bool `opt:"-e"`
+ Background bool `opt:"-b"`
+ Accounts bool `opt:"-a"`
+ Directories bool `opt:"-d"`
+ Command string `opt:"-c"`
+ Xargs string `opt:"..." complete:"CompleteXargs"`
+}
+
+func init() {
+ Register(Menu{})
+}
+
+func (Menu) Context() CommandContext {
+ return GLOBAL
+}
+
+func (Menu) Aliases() []string {
+ return []string{"menu"}
+}
+
+func (*Menu) CompleteXargs(arg string) []string {
+ return FilterList(ActiveCommandNames(), arg, nil)
+}
+
+func (m Menu) Execute([]string) error {
+ if m.Command == "" {
+ m.Command = config.General.DefaultMenuCmd
+ }
+ if m.Command == "" {
+ return errors.New(
+ "Either -c <command> or default-menu-cmd is required.")
+ }
+ if _, _, err := ResolveCommand(m.Xargs, nil, nil); err != nil {
+ return err
+ }
+
+ lines, err := m.feedLines()
+ if err != nil {
+ return err
+ }
+
+ pick, err := os.CreateTemp("", "aerc-menu-*")
+ if err != nil {
+ return err
+ }
+
+ var proc *exec.Cmd
+ if strings.Contains(m.Command, "%f") {
+ proc = exec.Command("sh", "-c",
+ strings.ReplaceAll(m.Command, "%f", opt.QuoteArg(pick.Name())))
+ } else {
+ proc = exec.Command("sh", "-c", m.Command+" >&3")
+ proc.ExtraFiles = append(proc.ExtraFiles, pick)
+ }
+ if len(lines) > 0 {
+ proc.Stdin = strings.NewReader(strings.Join(lines, "\n"))
+ }
+
+ xargs := func(err error) {
+ var buf []byte
+ if err == nil {
+ _, err = pick.Seek(0, io.SeekStart)
+ }
+ if err == nil {
+ buf, err = io.ReadAll(pick)
+ }
+ pick.Close()
+ os.Remove(pick.Name())
+ if err != nil {
+ app.PushError("command failed: " + err.Error())
+ return
+ }
+ if len(buf) == 0 {
+ return
+ }
+ var cmd Command
+ var cmdline string
+
+ for _, line := range strings.Split(string(buf), "\n") {
+ line = strings.TrimSpace(line)
+ if line == "" {
+ continue
+ }
+ cmdline = m.Xargs + " " + line
+ cmdline, cmd, err = ResolveCommand(cmdline, nil, nil)
+ if err == nil {
+ err = ExecuteCommand(cmd, cmdline)
+ }
+ if err != nil {
+ app.PushError(m.Xargs + ": " + err.Error())
+ if m.ErrExit {
+ return
+ }
+ }
+ }
+ }
+
+ if m.Background {
+ go func() {
+ defer log.PanicHandler()
+ xargs(proc.Run())
+ }()
+ } else {
+ term, err := app.NewTerminal(proc)
+ if err != nil {
+ return err
+ }
+ term.Focus(true)
+ term.OnClose = func(err error) {
+ app.CloseDialog()
+ xargs(err)
+ }
+
+ title := " :" + strings.TrimLeft(m.Xargs, ": \t") + " ... "
+
+ app.AddDialog(app.NewDialog(
+ ui.NewBox(term, title, "", app.SelectedAccountUiConfig()),
+ // start pos on screen
+ func(h int) int {
+ return h / 4
+ },
+ // dialog height
+ func(h int) int {
+ return h / 2
+ },
+ ))
+ }
+
+ return nil
+}
+
+func (m Menu) feedLines() ([]string, error) {
+ var lines []string
+
+ switch {
+ case m.Accounts && m.Directories:
+ for _, a := range app.AccountNames() {
+ account, _ := app.Account(a)
+ a = opt.QuoteArg(a)
+ for _, d := range account.Directories().List() {
+ dir := account.Directories().Directory(d)
+ if dir != nil && dir.Role != models.QueryRole {
+ d = opt.QuoteArg(d)
+ }
+ lines = append(lines, a+" "+d)
+ }
+ }
+
+ case m.Accounts:
+ for _, account := range app.AccountNames() {
+ lines = append(lines, opt.QuoteArg(account))
+ }
+
+ case m.Directories:
+ account := app.SelectedAccount()
+ if account == nil {
+ return nil, errors.New("No account selected.")
+ }
+ for _, d := range account.Directories().List() {
+ dir := account.Directories().Directory(d)
+ if dir != nil && dir.Role != models.QueryRole {
+ d = opt.QuoteArg(d)
+ }
+ lines = append(lines, d)
+ }
+ }
+
+ return lines, nil
+}
diff --git a/config/aerc.conf b/config/aerc.conf
index 92b92b8e..30c3c7bb 100644
--- a/config/aerc.conf
+++ b/config/aerc.conf
@@ -48,6 +48,21 @@
# Default: false
#enable-osc8=false
+# Default shell command to use for :menu. This will be executed with sh -c and
+# will run in an popover dialog.
+#
+# Any occurrence of %f will be replaced by a temporary file path where the
+# command is expected to write output lines to be consumed by :menu. Otherwise,
+# the lines will be read from the command's standard output.
+#
+# Examples:
+# default-menu-cmd=fzf
+# default-menu-cmd=fzf --multi
+# default-menu-cmd=dmenu -l 20
+# default-menu-cmd=ranger --choosefiles=%f
+#
+#default-menu-cmd=
+
[ui]
#
# Describes the format for each row in a mailbox view. This is a comma
diff --git a/config/general.go b/config/general.go
index f4149583..93794b6a 100644
--- a/config/general.go
+++ b/config/general.go
@@ -19,6 +19,7 @@ type GeneralConfig struct {
DisableIPC bool `ini:"disable-ipc"`
EnableOSC8 bool `ini:"enable-osc8" default:"false"`
Term string `ini:"term" default:"xterm-256color"`
+ DefaultMenuCmd string `ini:"default-menu-cmd"`
}
var General = new(GeneralConfig)
diff --git a/doc/aerc-config.5.scd b/doc/aerc-config.5.scd
index a616160f..5acb9427 100644
--- a/doc/aerc-config.5.scd
+++ b/doc/aerc-config.5.scd
@@ -79,6 +79,17 @@ These options are configured in the *[general]* section of _aerc.conf_.
Default: _false_
+*default-menu-cmd* = _<cmd>_
+ Default shell command to use for *:menu*. This will be executed with
+ _sh -c_ and will run in an popover dialog.
+
+ Any occurrence of _%f_ will be replaced by a temporary file path where
+ the command is expected to write output lines to be consumed by *:menu*.
+ Otherwise, the lines will be read from the command's standard output.
+
+ Example:
+ *default-menu-cmd* = _fzf_
+
# UI OPTIONS
These options are configured in the *[ui]* section of _aerc.conf_.
diff --git a/doc/aerc.1.scd b/doc/aerc.1.scd
index 63ce6345..ee5b27c5 100644
--- a/doc/aerc.1.scd
+++ b/doc/aerc.1.scd
@@ -170,6 +170,56 @@ These commands work in any context.
passed as one argument to the command, unless it is empty, in which case no
extra argument is added.
+*:menu* [*-c* _"<shell-cmd>"_] [*-e*] [*-b*] [*-a*] [*-d*] _<aerc-cmd ...>_
+ Opens a popover dialog running _sh -c "<shell-cmd>"_ (if not specified
+ *[general].default-menu-cmd* will be used). When the command exits, all
+ lines printed on its standard output will be appended to _<aerc-cmd ...>_
+ and executed as a standard aerc command like *xargs*(1) would do when
+ used in a shell. A colon (*:*) prefix is supported for _<aerc-cmd ...>_
+ but is not required.
+
+ *-c* _"<shell-cmd>"_
+ Override *[general].default-menu-cmd*. See *aerc-config*(5) for
+ more details.
+
+ *-e*: Stop executing commands on the first error.
+
+ *-b*: Do *NOT* spawn the popover dialog. Start the commands in the
+ background (*NOT* in a virtual terminal). Use this if _<shell-cmd>_ is
+ a graphical application that does not need a terminal.
+
+ _<shell-cmd>_ may be fed with input text using the following flags:
+ *-a*: All account names, one per line. E.g.:
+
+ '<account>' LF
+
+ *-d*: All current account directory names, one per line. E.g.:
+
+ '<directory>' LF
+
+ *-ad*: All directories of all accounts, one per line. E.g.:
+
+ '<account>' '<directory>' LF
+
+ Quotes may be added by aerc when either tokens contain special
+ characters. The quotes should be preserved for _<aerc-cmd ...>_.
+
+ Examples:
+
+ ```
+ :menu -adc fzf :cf -a
+ :menu -c 'fzf --multi' :attach
+ :menu -dc 'fzf --multi' :cp
+ :menu -bc 'dmenu -l 20' :cf
+ :menu -c 'ranger --choosefiles=%f' :attach
+ ```
+
+ This may also be used in key bindings (see *aerc-binds*(5)):
+
+ ```
+ <C-p> = :menu -adc fzf :cf -a<Enter>
+ ```
+
*:choose* *-o* _<key>_ _<text>_ _<command>_ [*-o* _<key>_ _<text>_ _<command>_]...
Prompts the user to choose from various options.
diff --git a/lib/ui/box.go b/lib/ui/box.go
index 96b95d59..e13b70f3 100644
--- a/lib/ui/box.go
+++ b/lib/ui/box.go
@@ -45,6 +45,7 @@ func (b *Box) Draw(ctx *Context) {
ctx.Printf(0, h-1, style, "%c%s%c", box[5], strings.Repeat(string(box[6]), w-2), box[7])
if b.title != "" && w > 4 {
+ style = b.uiConfig.GetStyle(config.STYLE_TITLE)
title := runewidth.Truncate(b.title, w-4, "…")
ctx.Printf(2, 0, style, "%s", title)
}