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 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 }