diff options
-rw-r--r-- | commands/commands.go | 2 | ||||
-rw-r--r-- | commands/menu.go | 187 | ||||
-rw-r--r-- | config/aerc.conf | 15 | ||||
-rw-r--r-- | config/general.go | 1 | ||||
-rw-r--r-- | doc/aerc-config.5.scd | 11 | ||||
-rw-r--r-- | doc/aerc.1.scd | 50 | ||||
-rw-r--r-- | lib/ui/box.go | 1 |
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) } |