aboutsummaryrefslogblamecommitdiffstats
path: root/commands/menu.go
blob: e14e4e83ae263947f58149431f0376677b4f8480 (plain) (tree)













































                                                               




                                                                









                                                                       





                                                                   

































                                                                                       
                                     

















                                                  
























                                                                                   
 






















                                                                     
         
 
 














                                                                                 








































                                                                               
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
	}
	useFallback := m.useFallback()
	if m.Background && useFallback {
		return errors.New("Either -c <command> or " +
			"default-menu-cmd is required to run " +
			"in the background.")
	}
	if _, _, err := ResolveCommand(m.Xargs, nil, nil); err != nil {
		return err
	}

	lines, err := m.feedLines()
	if err != nil {
		return err
	}

	title := " :" + strings.TrimLeft(m.Xargs, ": \t") + " ... "

	if useFallback {
		return m.fallback(title, lines)
	}

	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
		}
		m.runCmd(string(buf))
	}

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

		widget := ui.NewBox(term, title, "", app.SelectedAccountUiConfig())
		app.AddDialog(app.DefaultDialog(widget))
	}

	return nil
}

func (m Menu) useFallback() bool {
	if m.Command == "" || m.Command == "-" {
		warnMsg := "no command provided, falling back on aerc's picker."
		log.Warnf(warnMsg)
		app.PushWarning(warnMsg)
		return true
	}
	cmd, _, _ := strings.Cut(m.Command, " ")
	_, err := exec.LookPath(cmd)
	if err != nil {
		warnMsg := "command '" + cmd + "' not found in PATH, " +
			"falling back on aerc's picker."
		log.Warnf(warnMsg)
		app.PushWarning(warnMsg)
		return true
	}
	return false
}

func (m Menu) runCmd(buffer string) {
	var (
		cmd     Command
		cmdline string
		err     error
	)

	for _, line := range strings.Split(buffer, "\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
			}
		}
	}
}

func (m Menu) fallback(title string, lines []string) error {
	listBox := app.NewListBox(
		title, lines, app.SelectedAccountUiConfig(),
		func(line string) {
			app.CloseDialog()
			if line == "" {
				return
			}
			m.runCmd(line)
		})
	listBox.SetTextFilter(func(list []string, term string) []string {
		return FilterList(list, term, func(s string) string { return s })
	})
	widget := ui.NewBox(listBox, "", "", app.SelectedAccountUiConfig())
	app.AddDialog(app.DefaultDialog(widget))
	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
}